86 Commits

Author SHA1 Message Date
tecnovert
a6e7d36e84 Add incoming mweb->plain amounts to unconfirmed balance 2024-02-09 23:49:57 +02:00
tecnovert
6e4feb33d7 ui: Rename unconfirmed balance to pending, include immature balance. 2024-02-09 23:48:53 +02:00
tecnovert
1f810fab6b Raise version to 0.12.7 2024-02-09 11:36:50 +02:00
tecnovert
7bf5f7ddfa prepare: Avoid setting tor mode for commands that only modify config. 2024-02-09 11:17:30 +02:00
tecnovert
d7da532111 prepare: Deduplicate Monero tor config 2024-02-09 11:17:29 +02:00
tecnovert
fa35102794 prepare: Select aarch64 releases 2024-02-09 11:17:29 +02:00
tecnovert
da8c3e0237 prepare: Make default wshost match htmlhost 2024-02-09 11:17:29 +02:00
tecnovert
ec29c9889c Set monero-wallet-rpc proxy with parameter instead of config. 2024-02-09 11:17:29 +02:00
tecnovert
14298d022a Don't connect to XMR nodes at private ips over tor by default. 2024-02-09 11:17:28 +02:00
tecnovert
5ceaab57d1 Set trusted-daemon for XMR node in basicswap.json
basicswap-prepare will set trusted_daemon true if the daemon host address is a local ip address else false.
Override with --trustremotenode
--trustremotenode can be used with --enabletor
2024-02-09 11:17:28 +02:00
tecnovert
a5c3534c19 Better error message when trying to swap Firo <> XMR. 2024-02-09 11:17:28 +02:00
tecnovert
3b6f72c084 Note source of XMR rpc error messages. 2024-02-09 11:17:28 +02:00
tecnovert
7c0ea36e37 Add request-sent to transient errors list. 2024-02-09 11:17:28 +02:00
tecnovert
14577f7741 tests: Fix test_wallet_restore for LTC multiwallet 2024-02-09 11:17:27 +02:00
tecnovert
dbf3f8f034 prepare: Don't use bind and nolisten together. 2024-02-09 11:17:27 +02:00
tecnovert
b85d234a0b prepare: Switch Litecoin download url to github. 2024-02-09 11:17:27 +02:00
tecnovert
1bfb271b87 Add settings for Monero rpc timeouts. 2024-02-09 11:17:27 +02:00
tecnovert
f9bc5d46af prepare: Automatically set --usetorproxy if use_tor is set in basicswap.json 2024-02-09 11:17:27 +02:00
tecnovert
fab89a42f3 doc: Simplify tor install notes. 2024-02-09 11:17:26 +02:00
tecnovert
3e14a784f3 Connect to remote XMR daemons over tor when enabled. 2024-02-09 11:17:26 +02:00
nahuhh
e4f196411a fix for remote monero daemons on tor enabled installs 2024-02-09 11:17:26 +02:00
tecnovert
8318961f0b refactor: Replace struct.pack/unpack. 2024-02-02 18:53:44 +02:00
tecnovert
7c9504e0cd Merge branch 'nahuhh-monero' 2024-02-02 12:08:46 +02:00
nahuhh
98f3a52daa monero: blocks_confirmed (release funds after) change 7 to 3 2024-02-01 18:06:58 -05:00
tecnovert
3276f9abd4 Merge pull request #45 from nahuhh/litecoin
add ltc min_fee_relay
2024-02-01 15:55:01 +00:00
tecnovert
041ab18288 Fix min_relay_fee override. 2024-02-01 11:28:21 +02:00
tecnovert
d57366c0b2 Prevent multiple LOCK_TX_B_SEEN events, use rpcwallet for lockunspent. 2024-02-01 00:58:14 +02:00
tecnovert
1ec1764012 ui: Don't list LTC MWEB yet on offers page. 2024-01-31 21:16:42 +02:00
tecnovert
28fc4817c0 debug: Add ui option to schedule bid actions. 2024-01-31 20:40:22 +02:00
nahuhh
2d1bd87b41 add ltc min_fee_relay 2024-01-31 10:25:22 -05:00
tecnovert
9ee6669179 ui: Display count of locked UTXOs on wallet page. 2024-01-28 20:16:30 +02:00
tecnovert
30a5ea1652 tests: Run more tests in ci. 2024-01-28 00:07:25 +02:00
tecnovert
53ceae718b doc: Add Cirrus CI notes. 2024-01-27 14:22:40 +02:00
tecnovert
1af9f64020 Merge pull request #43 from nahuhh/update_monero
Update monero 0.18.2.2 to 0.18.3.1
2024-01-26 21:31:01 +00:00
nahuhh
cfd2151c1a Update monero 0.18.2.2 to 0.18.3.1 2024-01-26 07:05:36 -05:00
tecnovert
237d12afa0 Add min_relay_fee for LTC 2024-01-25 23:32:44 +02:00
gerlofvanek
a0cdd8cec9 ui: Ethereum Icons 2024-01-25 23:15:39 +02:00
tecnovert
1754650e75 Merge remote-tracking branch 'crz/dev' into dev 2024-01-25 21:10:20 +02:00
gerlofvanek
3b8b512003 ui: Update 2024-01-25 17:50:06 +01:00
tecnovert
81649dcf9b Set reverse bid state to error when bidaccept fails. 2024-01-25 17:04:49 +02:00
tecnovert
f5d4b8dc0d Show error when auto-accepting a bid fails. 2024-01-24 23:12:18 +02:00
tecnovert
bcfd63037a Add limits to time delay settings. 2024-01-22 09:22:09 +02:00
tecnovert
ddf3734f9d doc: Add missing dependencies. 2024-01-19 20:33:11 +02:00
tecnovert
8c07ee5108 ui: Fix obscured svg text. 2024-01-17 20:42:58 +02:00
tecnovert
6ad5880ba4 Remove bittrex.com rate source. 2024-01-17 20:25:28 +02:00
tecnovert
0ff0a13a67 coins: Fix LTC windows assert URL 2024-01-12 18:13:14 +02:00
tecnovert
ff7e8fe0aa Add coin name to getWalletInfo failed warning. 2024-01-07 09:12:59 +02:00
tecnovert
1068694990 Fix errors when one coin wallet fails to unlock. 2024-01-04 10:41:56 +02:00
tecnovert
66d1abd888 Fix settings apply. 2024-01-02 00:33:27 +02:00
gerlofvanek
671e626551 ui: Updated LTC coin icons, JS fixes.
- Fixed display of LTC MWEB coin icons.
- Fixes JS errors with rates table.
- Fix LTC and LTC-MWEB error with lookup rates (JSON)
2023-12-31 13:14:38 +01:00
tecnovert
192aff221e coins: Update Bitcoin version. 2023-12-31 02:27:05 +02:00
tecnovert
03fdf44220 Fix unlock. 2023-12-30 10:31:41 +02:00
tecnovert
38fa498b0b coins: Add LTC MWEB wallet 2023-12-29 15:36:00 +02:00
tecnovert
7547587d4e coins: Update Firo version to 0.14.13.1 2023-12-24 21:12:53 +02:00
tecnovert
fd0772f893 prepare: Fix Dash download url for osx64. 2023-12-22 17:31:05 +02:00
tecnovert
0a12625290 ui: Define default chart api key in one place. 2023-12-22 15:23:36 +02:00
gerlofvanek
d6ed5ba24c ui: Chart API update
- Updated with a new default Chart API Key
- Implemented Chart error handling
2023-12-22 13:26:29 +01:00
tecnovert
fb48797298 tests: Fix lint issues. 2023-12-19 13:21:15 +02:00
tecnovert
5bec1c31da Add 'timed out' to list of transient errors. 2023-12-19 12:58:24 +02:00
tecnovert
3d3fcbde0b doc: Make a local xmr node the default option. 2023-12-19 11:13:41 +02:00
tecnovert
61845a7a84 tests: Prune Firo tests. 2023-12-19 01:30:52 +02:00
tecnovert
65c93eaee6 coins: Update valid Firo swap types. 2023-12-18 23:45:55 +02:00
tecnovert
6a26f72bca coins: Update Dash to v20.0.2 2023-12-18 15:15:16 +02:00
tecnovert
0a9db22828 Replace all hashlib ripemd160 functions. 2023-12-16 09:38:34 +02:00
tecnovert
08f0156b75 guix: Raise bsx version 2023-12-14 21:40:31 +02:00
tecnovert
15bf9b2187 Raise coincurve version. 2023-12-14 20:44:29 +02:00
tecnovert
258b730c41 scripts: Print errors if offer/bid creation fails. 2023-12-09 13:25:03 +02:00
tecnovert
b409fe9f0e prepare: Fix Firo osx download url. 2023-12-08 15:06:33 +02:00
tecnovert
01bb3870b6 tests: Add gettxout and scantxoutset tests to test_btc_xmr.py 2023-12-02 19:32:39 +02:00
tecnovert
9efb244952 Shorten time format in ui and log. 2023-12-01 22:40:35 +02:00
tecnovert
0be5a4fca7 Add min_relay_fee option. 2023-12-01 19:16:28 +02:00
tecnovert
a4c79fb7aa aOnly set reversed flag for ads type swaps. 2023-12-01 14:46:57 +02:00
tecnovert
5b6f447692 tests: Update swap type validation. 2023-12-01 09:23:10 +02:00
tecnovert
28baa597cc coins: Read Firo ProgPow blocks 2023-12-01 00:19:54 +02:00
tecnovert
d4a6ad7d6f Enable reverse adaptor sig swaps for segwit-less coins. 2023-11-30 21:46:03 +02:00
tecnovert
ce578f8025 Always use csv with adaptor sig swaps. 2023-11-30 18:16:24 +02:00
tecnovert
c387bfec71 coins: Remove asyncore code. 2023-11-28 20:05:31 +02:00
tecnovert
d668a2f342 prepare: Set core_version_group correctly for Firo. 2023-11-28 17:38:30 +02:00
tecnovert
8e17ee5939 coins: Raise Particl Version 2023-11-28 02:23:56 +02:00
tecnovert
3b55d17a26 Use special Firo release: 0.14.13.0-firod-only, new env var: SKIP_GPG_VALIDATION 2023-11-27 19:09:20 +02:00
tecnovert
fd0bf9ed73 Raise version to 0.12.1 2023-11-25 11:08:00 +02:00
tecnovert
e6c7c4d9bb Revert to Firo binary.
The .dmg file only contains Firo-qt
2023-11-25 00:55:41 +02:00
tecnovert
7053d7ee4b Use gettxout where scantxoutset is not available. 2023-11-25 00:40:52 +02:00
tecnovert
22cd3cf9f1 test: Fix selenium tests. 2023-11-09 22:34:50 +02:00
tecnovert
05e6edd5df ui: Fix applying XMR settings with an empty remote urls list. 2023-11-09 21:24:57 +02:00
tecnovert
9a1b7db2dc Print python version. 2023-10-17 14:42:52 +02:00
100 changed files with 8725 additions and 2843 deletions

View File

@@ -24,7 +24,7 @@ test_task:
- apt-get install -y wget python3-pip gnupg unzip protobuf-compiler automake libtool pkg-config
- pip install tox pytest
- python3 setup.py install
- wget -O coincurve-anonswap.zip https://github.com/tecnovert/coincurve/archive/refs/tags/anonswap_v0.1.zip
- wget -O coincurve-anonswap.zip https://github.com/tecnovert/coincurve/archive/refs/tags/anonswap_v0.2.zip
- unzip -d coincurve-anonswap coincurve-anonswap.zip
- mv ./coincurve-anonswap/*/{.,}* ./coincurve-anonswap || true
- cd coincurve-anonswap
@@ -47,3 +47,4 @@ test_task:
- pytest tests/basicswap/test_other.py
- pytest tests/basicswap/test_run.py
- pytest tests/basicswap/test_reload.py
- pytest tests/basicswap/test_btc_xmr.py -k 'test_01_a or test_01_b or test_02_a or test_02_b'

3
.gitignore vendored
View File

@@ -9,3 +9,6 @@ __pycache__
.tox
.eggs
*~
# geckodriver.log
*.log

View File

@@ -22,7 +22,7 @@ before_install:
install:
- travis_retry pip install tox pytest
before_script:
- wget -O coincurve-anonswap.zip https://github.com/tecnovert/coincurve/archive/refs/tags/anonswap_v0.1.zip
- wget -O coincurve-anonswap.zip https://github.com/tecnovert/coincurve/archive/refs/tags/anonswap_v0.2.zip
- unzip -d coincurve-anonswap coincurve-anonswap.zip
- mv ./coincurve-anonswap/*/{.,}* ./coincurve-anonswap || true
- cd coincurve-anonswap

View File

@@ -15,7 +15,7 @@ RUN wget -O protobuf_src.tar.gz https://github.com/protocolbuffers/protobuf/rele
make -j$(nproc) install && \
ldconfig
ARG COINCURVE_VERSION=v0.1
ARG COINCURVE_VERSION=v0.2
RUN wget -O coincurve-anonswap.zip https://github.com/tecnovert/coincurve/archive/refs/tags/anonswap_$COINCURVE_VERSION.zip && \
unzip coincurve-anonswap.zip && \
mv ./coincurve-anonswap_$COINCURVE_VERSION ./coincurve-anonswap && \

View File

@@ -1,5 +1,5 @@
The MIT License (MIT)
Copyright (c) 2019 tecnovert
Copyright (c) 2019-2024 tecnovert
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -1,3 +1,3 @@
name = "basicswap"
__version__ = "0.11.68"
__version__ = "0.12.7"

View File

@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019-2023 tecnovert
# Copyright (c) 2019-2024 tecnovert
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -8,6 +8,7 @@ import os
import time
import shlex
import socks
import random
import socket
import urllib
import logging
@@ -76,10 +77,10 @@ class BaseApp:
# Remove any existing handlers
self.log.handlers = []
formatter = logging.Formatter('%(asctime)s %(levelname)s : %(message)s')
formatter = logging.Formatter('%(asctime)s %(levelname)s : %(message)s', '%Y-%m-%d %H:%M:%S')
stream_stdout = logging.StreamHandler()
if self.log_name != 'BasicSwap':
stream_stdout.setFormatter(logging.Formatter('%(asctime)s %(name)s %(levelname)s : %(message)s'))
stream_stdout.setFormatter(logging.Formatter('%(asctime)s %(name)s %(levelname)s : %(message)s', '%Y-%m-%d %H:%M:%S'))
else:
stream_stdout.setFormatter(formatter)
stream_fp = logging.StreamHandler(self.fp)
@@ -198,3 +199,28 @@ class BaseApp:
def setMockTimeOffset(self, new_offset: int) -> None:
self.log.warning(f'Setting mocktime to {new_offset}')
self.mock_time_offset = new_offset
def get_int_setting(self, name: str, default_v: int, min_v: int, max_v) -> int:
value: int = self.settings.get(name, default_v)
if value < min_v:
self.log.warning(f'Setting {name} to {min_v}')
value = min_v
if value > max_v:
self.log.warning(f'Setting {name} to {max_v}')
value = max_v
return value
def get_delay_event_seconds(self):
if self.min_delay_event == self.max_delay_event:
return self.min_delay_event
return random.randrange(self.min_delay_event, self.max_delay_event)
def get_short_delay_event_seconds(self):
if self.min_delay_event_short == self.max_delay_event_short:
return self.min_delay_event_short
return random.randrange(self.min_delay_event_short, self.max_delay_event_short)
def get_delay_retry_seconds(self):
if self.min_delay_retry == self.max_delay_retry:
return self.min_delay_retry
return random.randrange(self.min_delay_retry, self.max_delay_retry)

File diff suppressed because it is too large Load Diff

View File

@@ -180,6 +180,7 @@ class EventLogTypes(IntEnum):
PTX_PUBLISHED = auto()
PTX_REDEEM_PUBLISHED = auto()
PTX_REFUND_PUBLISHED = auto()
LOCK_TX_B_IN_MEMPOOL = auto()
class XmrSplitMsgTypes(IntEnum):
@@ -198,6 +199,7 @@ class DebugTypes(IntEnum):
SKIP_LOCK_TX_REFUND = auto()
SEND_LOCKED_XMR = auto()
B_LOCK_TX_MISSED_SEND = auto()
DUPLICATE_ACTIONS = auto()
class NotificationTypes(IntEnum):
@@ -390,6 +392,8 @@ def describeEventEntry(event_type, event_msg):
return 'Lock tx B seen in chain'
if event_type == EventLogTypes.LOCK_TX_B_CONFIRMED:
return 'Lock tx B confirmed in chain'
if event_type == EventLogTypes.LOCK_TX_B_IN_MEMPOOL:
return 'Lock tx B seen in mempool'
if event_type == EventLogTypes.DEBUG_TWEAK_APPLIED:
return 'Debug tweak applied ' + event_msg
if event_type == EventLogTypes.FAILED_TX_B_REFUND:

View File

@@ -32,6 +32,7 @@ class Coins(IntEnum):
DASH = 12
FIRO = 13
NAV = 14
LTC_MWEB = 15
chainparams = {
@@ -219,6 +220,7 @@ chainparams = {
'message_magic': 'DarkNet Signed Message:\n',
'blocks_target': 60 * 1,
'decimal_places': 8,
'has_cltv': True,
'has_csv': False,
'has_segwit': False,
'use_ticker_as_name': True,
@@ -296,7 +298,8 @@ chainparams = {
'message_magic': 'Zcoin Signed Message:\n',
'blocks_target': 60 * 10,
'decimal_places': 8,
'has_csv': True,
'has_cltv': False,
'has_csv': False,
'has_segwit': False,
'mainnet': {
'rpcport': 8888,
@@ -462,4 +465,8 @@ class CoinInterface:
return True
if 'daemon is busy' in str_error:
return True
if 'timed out' in str_error:
return True
if 'request-sent' in str_error:
return True
return False

View File

@@ -12,7 +12,7 @@ from enum import IntEnum, auto
from sqlalchemy.ext.declarative import declarative_base
CURRENT_DB_VERSION = 21
CURRENT_DB_VERSION = 22
CURRENT_DB_DATA_VERSION = 4
Base = declarative_base()
@@ -67,6 +67,7 @@ class Offer(Base):
proof_address = sa.Column(sa.String)
proof_signature = sa.Column(sa.LargeBinary)
proof_utxos = sa.Column(sa.LargeBinary)
pkhash_seller = sa.Column(sa.LargeBinary)
secret_hash = sa.Column(sa.LargeBinary)
@@ -115,6 +116,7 @@ class Bid(Base):
expire_at = sa.Column(sa.BigInteger)
bid_addr = sa.Column(sa.String)
proof_address = sa.Column(sa.String)
proof_utxos = sa.Column(sa.LargeBinary)
withdraw_to_addr = sa.Column(sa.String) # Address to spend lock tx to - address from wallet if empty TODO
recovered_secret = sa.Column(sa.LargeBinary)

View File

@@ -293,6 +293,10 @@ def upgradeDatabase(self, db_version):
msg_id BLOB,
PRIMARY KEY (record_id))''')
session.execute('ALTER TABLE offers ADD COLUMN bid_reversed INTEGER')
elif current_version == 21:
db_version += 1
session.execute('ALTER TABLE offers ADD COLUMN proof_utxos BLOB')
session.execute('ALTER TABLE bids ADD COLUMN proof_utxos BLOB')
if current_version != db_version:
self.db_version = db_version

View File

@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019-2023 tecnovert
# Copyright (c) 2019-2024 tecnovert
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -280,6 +280,8 @@ class HttpHandler(BaseHTTPRequestHandler):
coin_id = int(get_data_entry(form_data, 'coin_type'))
if coin_id in (-2, -3, -4):
coin_type = Coins(Coins.XMR)
elif coin_id in (-5,):
coin_type = Coins(Coins.LTC)
else:
coin_type = Coins(coin_id)
except Exception:
@@ -295,20 +297,23 @@ class HttpHandler(BaseHTTPRequestHandler):
method = arr[0]
params = json.loads(arr[1]) if len(arr) > 1 else []
if coin_id == -4:
rv = ci.rpc_wallet_cb(method, params)
rv = ci.rpc_wallet(method, params)
elif coin_id == -3:
rv = ci.rpc_cb(method, params)
rv = ci.rpc(method, params)
elif coin_id == -2:
if params == []:
params = None
rv = ci.rpc_cb2(method, params)
rv = ci.rpc2(method, params)
else:
raise ValueError('Unknown XMR RPC variant')
result = json.dumps(rv, indent=4)
else:
if call_type == 'http':
method, params = parse_cmd(cmd, type_map)
rv = swap_client.ci(coin_type).rpc_callback(method, params)
if coin_id == -5:
rv = swap_client.ci(coin_type).rpc_wallet_mweb(method, params)
else:
rv = swap_client.ci(coin_type).rpc_wallet(method, params)
if not isinstance(rv, str):
rv = json.dumps(rv, indent=4)
result = cmd + '\n' + rv
@@ -322,10 +327,15 @@ class HttpHandler(BaseHTTPRequestHandler):
template = env.get_template('rpc.html')
coins = listAvailableCoins(swap_client, with_variants=False)
with_xmr: bool = any(c[0] == Coins.XMR for c in coins)
coins = [c for c in coins if c[0] != Coins.XMR]
coins.append((-2, 'Monero'))
coins.append((-3, 'Monero JSON'))
coins.append((-4, 'Monero Wallet'))
if any(c[0] == Coins.LTC for c in coins):
coins.append((-5, 'Litecoin MWEB Wallet'))
if with_xmr:
coins.append((-2, 'Monero'))
coins.append((-3, 'Monero JSON'))
coins.append((-4, 'Monero Wallet'))
return self.render_template(template, {
'messages': messages,

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (c) 2020-2023 tecnovert
# Copyright (c) 2020-2024 tecnovert
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -195,7 +195,9 @@ class BTCInterface(CoinInterface):
self._rpc_host = coin_settings.get('rpchost', '127.0.0.1')
self._rpcport = coin_settings['rpcport']
self._rpcauth = coin_settings['rpcauth']
self.rpc_callback = make_rpc_func(self._rpcport, self._rpcauth, host=self._rpc_host)
self.rpc = make_rpc_func(self._rpcport, self._rpcauth, host=self._rpc_host)
self._rpc_wallet = 'wallet.dat'
self.rpc_wallet = make_rpc_func(self._rpcport, self._rpcauth, host=self._rpc_host, wallet=self._rpc_wallet)
self.blocks_confirmed = coin_settings['blocks_confirmed']
self.setConfTarget(coin_settings['conf_target'])
self._use_segwit = coin_settings['use_segwit']
@@ -204,6 +206,23 @@ class BTCInterface(CoinInterface):
self._log = self._sc.log if self._sc and self._sc.log else logging
self._expect_seedid_hex = None
def checkWallets(self) -> int:
wallets = self.rpc('listwallets')
# Wallet name is "" for some LTC and PART installs on older cores
if self._rpc_wallet not in wallets and len(wallets) > 0:
self._log.debug('Changing {} wallet name.'.format(self.ticker()))
for wallet_name in wallets:
# Skip over other expected wallets
if wallet_name in ('mweb', ):
continue
self._rpc_wallet = wallet_name
self._log.info('Switched {} wallet name to {}.'.format(self.ticker(), self._rpc_wallet))
self.rpc_wallet = make_rpc_func(self._rpcport, self._rpcauth, host=self._rpc_host, wallet=self._rpc_wallet)
break
return len(wallets)
def using_segwit(self) -> bool:
# Using btc native segwit
return self._use_segwit
@@ -239,34 +258,34 @@ class BTCInterface(CoinInterface):
self._conf_target = new_conf_target
def testDaemonRPC(self, with_wallet=True) -> None:
self.rpc_callback('getwalletinfo' if with_wallet else 'getblockchaininfo')
self.rpc_wallet('getwalletinfo' if with_wallet else 'getblockchaininfo')
def getDaemonVersion(self):
return self.rpc_callback('getnetworkinfo')['version']
return self.rpc('getnetworkinfo')['version']
def getBlockchainInfo(self):
return self.rpc_callback('getblockchaininfo')
return self.rpc('getblockchaininfo')
def getChainHeight(self) -> int:
return self.rpc_callback('getblockcount')
return self.rpc('getblockcount')
def getMempoolTx(self, txid):
return self.rpc_callback('getrawtransaction', [txid.hex()])
return self.rpc('getrawtransaction', [txid.hex()])
def getBlockHeaderFromHeight(self, height):
block_hash = self.rpc_callback('getblockhash', [height])
return self.rpc_callback('getblockheader', [block_hash])
block_hash = self.rpc('getblockhash', [height])
return self.rpc('getblockheader', [block_hash])
def getBlockHeader(self, block_hash):
return self.rpc_callback('getblockheader', [block_hash])
return self.rpc('getblockheader', [block_hash])
def getBlockHeaderAt(self, time: int, block_after=False):
blockchaininfo = self.rpc_callback('getblockchaininfo')
last_block_header = self.rpc_callback('getblockheader', [blockchaininfo['bestblockhash']])
blockchaininfo = self.rpc('getblockchaininfo')
last_block_header = self.rpc('getblockheader', [blockchaininfo['bestblockhash']])
max_tries = 5000
for i in range(max_tries):
prev_block_header = self.rpc_callback('getblock', [last_block_header['previousblockhash']])
prev_block_header = self.rpc('getblock', [last_block_header['previousblockhash']])
if prev_block_header['time'] <= time:
return last_block_header if block_after else prev_block_header
@@ -275,20 +294,20 @@ class BTCInterface(CoinInterface):
def initialiseWallet(self, key_bytes: bytes) -> None:
key_wif = self.encodeKey(key_bytes)
self.rpc_callback('sethdseed', [True, key_wif])
self.rpc_wallet('sethdseed', [True, key_wif])
def getWalletInfo(self):
rv = self.rpc_callback('getwalletinfo')
rv = self.rpc_wallet('getwalletinfo')
rv['encrypted'] = 'unlocked_until' in rv
rv['locked'] = rv.get('unlocked_until', 1) <= 0
rv['locked_utxos'] = len(self.rpc_wallet('listlockunspent'))
return rv
def walletRestoreHeight(self) -> int:
return self._restore_height
def getWalletRestoreHeight(self) -> int:
start_time = self.rpc_callback('getwalletinfo')['keypoololdest']
start_time = self.rpc_wallet('getwalletinfo')['keypoololdest']
blockchaininfo = self.getBlockchainInfo()
best_block = blockchaininfo['bestblockhash']
@@ -312,7 +331,7 @@ class BTCInterface(CoinInterface):
raise ValueError('{} wallet restore height not found.'.format(self.coin_name()))
def getWalletSeedID(self) -> str:
wi = self.rpc_callback('getwalletinfo')
wi = self.rpc_wallet('getwalletinfo')
return 'Not found' if 'hdseedid' not in wi else wi['hdseedid']
def checkExpectedSeed(self, expect_seedid) -> bool:
@@ -323,11 +342,11 @@ class BTCInterface(CoinInterface):
args = [label]
if use_segwit:
args.append('bech32')
return self.rpc_callback('getnewaddress', args)
return self.rpc_wallet('getnewaddress', args)
def isValidAddress(self, address: str) -> bool:
try:
rv = self.rpc_callback('validateaddress', [address])
rv = self.rpc_wallet('validateaddress', [address])
if rv['isvalid'] is True:
return True
except Exception as ex:
@@ -347,29 +366,45 @@ class BTCInterface(CoinInterface):
return False
def isAddressMine(self, address: str, or_watch_only: bool = False) -> bool:
addr_info = self.rpc_callback('getaddressinfo', [address])
addr_info = self.rpc_wallet('getaddressinfo', [address])
if not or_watch_only:
return addr_info['ismine']
return addr_info['ismine'] or addr_info['iswatchonly']
def checkAddressMine(self, address: str) -> None:
addr_info = self.rpc_callback('getaddressinfo', [address])
addr_info = self.rpc_wallet('getaddressinfo', [address])
ensure(addr_info['ismine'], 'ismine is false')
if self.sc._restrict_unknown_seed_wallets:
ensure(addr_info['hdseedid'] == self._expect_seedid_hex, 'unexpected seedid')
def get_fee_rate(self, conf_target: int = 2):
try:
fee_rate = self.rpc_callback('estimatesmartfee', [conf_target])['feerate']
assert (fee_rate > 0.0), 'Non positive feerate'
return fee_rate, 'estimatesmartfee'
except Exception:
def get_fee_rate(self, conf_target: int = 2) -> (float, str):
chain_client_settings = self._sc.getChainClientSettings(self.coin_type()) # basicswap.json
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)
return override_feerate, 'override_feerate'
min_relay_fee = chain_client_settings.get('min_relay_fee', None)
def try_get_fee_rate(self, conf_target):
try:
fee_rate = self.rpc_callback('getwalletinfo')['paytxfee']
assert (fee_rate > 0.0), 'Non positive feerate'
return fee_rate, 'paytxfee'
fee_rate: float = self.rpc_wallet('estimatesmartfee', [conf_target])['feerate']
assert (fee_rate > 0.0), 'Negative feerate'
return fee_rate, 'estimatesmartfee'
except Exception:
return self.rpc_callback('getnetworkinfo')['relayfee'], 'relayfee'
try:
fee_rate: float = self.rpc_wallet('getwalletinfo')['paytxfee']
assert (fee_rate > 0.0), 'Non positive feerate'
return fee_rate, 'paytxfee'
except Exception:
fee_rate: float = self.rpc('getnetworkinfo')['relayfee']
return fee_rate, 'relayfee'
fee_rate, rate_src = try_get_fee_rate(self, conf_target)
if min_relay_fee and min_relay_fee > fee_rate:
self._log.warning('Feerate {} ({}) is below min relay fee {} for {}'.format(self.format_amount(fee_rate, True, 1), rate_src, self.format_amount(min_relay_fee, True, 1), self.coin_name()))
return min_relay_fee, 'min_relay_fee'
return fee_rate, rate_src
def isSegwitAddress(self, address: str) -> bool:
return address.startswith(self.chainparams_network()['hrp'] + '1')
@@ -720,7 +755,7 @@ class BTCInterface(CoinInterface):
add_bytes = 0
add_witness_bytes = getCompactSizeLen(len(tx.vin))
for pi in tx.vin:
ptx = self.rpc_callback('getrawtransaction', [i2h(pi.prevout.hash), True])
ptx = self.rpc('getrawtransaction', [i2h(pi.prevout.hash), True])
prevout = ptx['vout'][pi.prevout.n]
inputs_value += make_int(prevout['value'])
@@ -928,13 +963,13 @@ class BTCInterface(CoinInterface):
'lockUnspents': True,
'feeRate': feerate_str,
}
rv = self.rpc_callback('fundrawtransaction', [tx.hex(), options])
rv = self.rpc_wallet('fundrawtransaction', [tx.hex(), options])
return bytes.fromhex(rv['hex'])
def listInputs(self, tx_bytes):
tx = self.loadTx(tx_bytes)
all_locked = self.rpc_callback('listlockunspent')
all_locked = self.rpc_wallet('listlockunspent')
inputs = []
for pi in tx.vin:
txid_hex = i2h(pi.prevout.hash)
@@ -948,19 +983,19 @@ class BTCInterface(CoinInterface):
inputs = []
for pi in tx.vin:
inputs.append({'txid': i2h(pi.prevout.hash), 'vout': pi.prevout.n})
self.rpc_callback('lockunspent', [True, inputs])
self.rpc_wallet('lockunspent', [True, inputs])
def signTxWithWallet(self, tx: bytes) -> bytes:
rv = self.rpc_callback('signrawtransactionwithwallet', [tx.hex()])
rv = self.rpc_wallet('signrawtransactionwithwallet', [tx.hex()])
return bytes.fromhex(rv['hex'])
def signTxWithKey(self, tx: bytes, key: bytes) -> bytes:
key_wif = self.encodeKey(key)
rv = self.rpc_callback('signrawtransactionwithkey', [tx.hex(), [key_wif, ]])
rv = self.rpc('signrawtransactionwithkey', [tx.hex(), [key_wif, ]])
return bytes.fromhex(rv['hex'])
def publishTx(self, tx: bytes):
return self.rpc_callback('sendrawtransaction', [tx.hex()])
return self.rpc('sendrawtransaction', [tx.hex()])
def encodeTx(self, tx) -> bytes:
return tx.serialize()
@@ -1004,18 +1039,18 @@ class BTCInterface(CoinInterface):
return self.getScriptForPubkeyHash(self.getPubkeyHash(K))
def scanTxOutset(self, dest):
return self.rpc_callback('scantxoutset', ['start', ['raw({})'.format(dest.hex())]])
return self.rpc('scantxoutset', ['start', ['raw({})'.format(dest.hex())]])
def getTransaction(self, txid: bytes):
try:
return bytes.fromhex(self.rpc_callback('getrawtransaction', [txid.hex()]))
return bytes.fromhex(self.rpc('getrawtransaction', [txid.hex()]))
except Exception as ex:
# TODO: filter errors
return None
def getWalletTransaction(self, txid: bytes):
try:
return bytes.fromhex(self.rpc_callback('gettransaction', [txid.hex()]))
return bytes.fromhex(self.rpc('gettransaction', [txid.hex()]))
except Exception as ex:
# TODO: filter errors
return None
@@ -1101,7 +1136,7 @@ class BTCInterface(CoinInterface):
def spendBLockTx(self, chain_b_lock_txid: bytes, address_to: str, kbv: bytes, kbs: bytes, cb_swap_value: int, b_fee: int, restore_height: int) -> bytes:
self._log.info('spendBLockTx %s:\n', chain_b_lock_txid.hex())
wtx = self.rpc_callback('gettransaction', [chain_b_lock_txid.hex(), ])
wtx = self.rpc_wallet('gettransaction', [chain_b_lock_txid.hex(), ])
lock_tx = self.loadTx(bytes.fromhex(wtx['hex']))
Kbs = self.getPubkey(kbs)
@@ -1130,10 +1165,10 @@ class BTCInterface(CoinInterface):
return bytes.fromhex(self.publishTx(b_lock_spend_tx))
def importWatchOnlyAddress(self, address: str, label: str):
self.rpc_callback('importaddress', [address, label, False])
self.rpc_wallet('importaddress', [address, label, False])
def isWatchOnlyAddress(self, address: str):
addr_info = self.rpc_callback('getaddressinfo', [address])
addr_info = self.rpc_wallet('getaddressinfo', [address])
return addr_info['iswatchonly']
def getSCLockScriptAddress(self, lock_script):
@@ -1147,11 +1182,11 @@ class BTCInterface(CoinInterface):
self.importWatchOnlyAddress(dest_address, 'bid')
self._log.info('Imported watch-only addr: {}'.format(dest_address))
self._log.info('Rescanning {} chain from height: {}'.format(self.coin_name(), rescan_from))
self.rpc_callback('rescanblockchain', [rescan_from])
self.rpc_wallet('rescanblockchain', [rescan_from])
return_txid = True if txid is None else False
if txid is None:
txns = self.rpc_callback('listunspent', [0, 9999999, [dest_address, ]])
txns = self.rpc_wallet('listunspent', [0, 9999999, [dest_address, ]])
for tx in txns:
if self.make_int(tx['amount']) == bid_amount:
@@ -1162,11 +1197,11 @@ class BTCInterface(CoinInterface):
return None
try:
tx = self.rpc_callback('gettransaction', [txid.hex()])
tx = self.rpc_wallet('gettransaction', [txid.hex()])
block_height = 0
if 'blockhash' in tx:
block_header = self.rpc_callback('getblockheader', [tx['blockhash']])
block_header = self.rpc('getblockheader', [tx['blockhash']])
block_height = block_header['height']
rv = {
@@ -1178,7 +1213,7 @@ class BTCInterface(CoinInterface):
return None
if find_index:
tx_obj = self.rpc_callback('decoderawtransaction', [tx['hex']])
tx_obj = self.rpc('decoderawtransaction', [tx['hex']])
rv['index'] = find_vout_for_address_from_txobj(tx_obj, dest_address)
if return_txid:
@@ -1188,7 +1223,7 @@ class BTCInterface(CoinInterface):
def getOutput(self, txid, dest_script, expect_value, xmr_swap=None):
# TODO: Use getrawtransaction if txindex is active
utxos = self.rpc_callback('scantxoutset', ['start', ['raw({})'.format(dest_script.hex())]])
utxos = self.rpc('scantxoutset', ['start', ['raw({})'.format(dest_script.hex())]])
if 'height' in utxos: # chain_height not returned by v18 codebase
chain_height = utxos['height']
else:
@@ -1211,7 +1246,7 @@ class BTCInterface(CoinInterface):
def withdrawCoin(self, value, addr_to, subfee):
params = [addr_to, value, '', '', subfee, True, self._conf_target]
return self.rpc_callback('sendtoaddress', params)
return self.rpc_wallet('sendtoaddress', params)
def signCompact(self, k, message):
message_hash = hashlib.sha256(bytes(message, 'utf-8')).digest()
@@ -1304,10 +1339,10 @@ class BTCInterface(CoinInterface):
return length
def describeTx(self, tx_hex: str):
return self.rpc_callback('decoderawtransaction', [tx_hex])
return self.rpc('decoderawtransaction', [tx_hex])
def getSpendableBalance(self) -> int:
return self.make_int(self.rpc_callback('getbalances')['mine']['trusted'])
return self.make_int(self.rpc_wallet('getbalances')['mine']['trusted'])
def createUTXO(self, value_sats: int):
# Create a new address and send value_sats to it
@@ -1320,7 +1355,7 @@ class BTCInterface(CoinInterface):
return self.withdrawCoin(self.format_amount(value_sats), address, False), address
def createRawFundedTransaction(self, addr_to: str, amount: int, sub_fee: bool = False, lock_unspents: bool = True) -> str:
txn = self.rpc_callback('createrawtransaction', [[], {addr_to: self.format_amount(amount)}])
txn = self.rpc('createrawtransaction', [[], {addr_to: self.format_amount(amount)}])
options = {
'lockUnspents': lock_unspents,
@@ -1328,30 +1363,44 @@ class BTCInterface(CoinInterface):
}
if sub_fee:
options['subtractFeeFromOutputs'] = [0,]
return self.rpc_callback('fundrawtransaction', [txn, options])['hex']
return self.rpc_wallet('fundrawtransaction', [txn, options])['hex']
def createRawSignedTransaction(self, addr_to, amount) -> str:
txn_funded = self.createRawFundedTransaction(addr_to, amount)
return self.rpc_callback('signrawtransactionwithwallet', [txn_funded])['hex']
return self.rpc_wallet('signrawtransactionwithwallet', [txn_funded])['hex']
def getBlockWithTxns(self, block_hash: str):
return self.rpc_callback('getblock', [block_hash, 2])
return self.rpc('getblock', [block_hash, 2])
def getUnspentsByAddr(self):
unspent_addr = dict()
unspent = self.rpc_callback('listunspent')
unspent = self.rpc_wallet('listunspent')
for u in unspent:
if u['spendable'] is not True:
continue
if 'address' not in u:
continue
if 'desc' in u:
desc = u['desc']
if self.using_segwit:
if self.use_p2shp2wsh():
if not desc.startswith('sh(wpkh'):
continue
else:
if not desc.startswith('wpkh'):
continue
else:
if not desc.startswith('pkh'):
continue
unspent_addr[u['address']] = unspent_addr.get(u['address'], 0) + self.make_int(u['amount'], r=1)
return unspent_addr
def getUTXOBalance(self, address: str):
num_blocks = self.rpc_callback('getblockcount')
num_blocks = self.rpc('getblockcount')
sum_unspent = 0
self._log.debug('[rm] scantxoutset start') # scantxoutset is slow
ro = self.rpc_callback('scantxoutset', ['start', ['addr({})'.format(address)]]) # TODO: Use combo(address) where possible
ro = self.rpc('scantxoutset', ['start', ['addr({})'.format(address)]]) # TODO: Use combo(address) where possible
self._log.debug('[rm] scantxoutset end')
for o in ro['unspents']:
sum_unspent += self.make_int(o['amount'])
@@ -1377,11 +1426,22 @@ class BTCInterface(CoinInterface):
sign_for_addr = self.pkh_to_address(pkh)
self._log.debug('sign_for_addr converted %s', sign_for_addr)
signature = self.rpc_callback('signmessage', [sign_for_addr, sign_for_addr + '_swap_proof_' + extra_commit_bytes.hex()])
signature = self.rpc_wallet('signmessage', [sign_for_addr, sign_for_addr + '_swap_proof_' + extra_commit_bytes.hex()])
return (sign_for_addr, signature)
prove_utxos = [] # TODO: Send specific utxos
return (sign_for_addr, signature, prove_utxos)
def verifyProofOfFunds(self, address, signature, extra_commit_bytes):
def decodeProofUtxos(self, msg_utxos):
proof_utxos = []
if len(msg_utxos) > 0:
num_utxos = len(msg_utxos) // 34
p: int = 0
for i in range(num_utxos):
proof_utxos.append((msg_utxos[p: p + 32], int.from_bytes(msg_utxos[p + 32: p + 34], 'big')))
p += 34
return proof_utxos
def verifyProofOfFunds(self, address, signature, utxos, extra_commit_bytes):
passed = self.verifyMessage(address, address + '_swap_proof_' + extra_commit_bytes.hex(), signature)
ensure(passed is True, 'Proof of funds signature invalid')
@@ -1391,17 +1451,17 @@ class BTCInterface(CoinInterface):
return self.getUTXOBalance(address)
def isWalletEncrypted(self) -> bool:
wallet_info = self.rpc_callback('getwalletinfo')
wallet_info = self.rpc_wallet('getwalletinfo')
return 'unlocked_until' in wallet_info
def isWalletLocked(self) -> bool:
wallet_info = self.rpc_callback('getwalletinfo')
wallet_info = self.rpc_wallet('getwalletinfo')
if 'unlocked_until' in wallet_info and wallet_info['unlocked_until'] <= 0:
return True
return False
def isWalletEncryptedLocked(self):
wallet_info = self.rpc_callback('getwalletinfo')
wallet_info = self.rpc_wallet('getwalletinfo')
encrypted = 'unlocked_until' in wallet_info
locked = encrypted and wallet_info['unlocked_until'] <= 0
return encrypted, locked
@@ -1411,8 +1471,8 @@ class BTCInterface(CoinInterface):
if old_password == '':
if self.isWalletEncrypted():
raise ValueError('Old password must be set')
return self.rpc_callback('encryptwallet', [new_password])
self.rpc_callback('walletpassphrasechange', [old_password, new_password])
return self.rpc_wallet('encryptwallet', [new_password])
self.rpc_wallet('walletpassphrasechange', [old_password, new_password])
def unlockWallet(self, password: str):
if password == '':
@@ -1422,21 +1482,20 @@ class BTCInterface(CoinInterface):
if self.coin_type() == Coins.BTC:
# Recreate wallet if none found
# Required when encrypting an existing btc wallet, workaround is to delete the btc wallet and recreate
wallets = self.rpc_callback('listwallets')
wallets = self.rpc('listwallets')
if len(wallets) < 1:
self._log.info('Creating wallet.dat for {}.'.format(self.coin_name()))
# wallet_name, disable_private_keys, blank, passphrase, avoid_reuse, descriptors
self.rpc_callback('createwallet', ['wallet.dat', False, True, '', False, False])
self.rpc_callback('encryptwallet', [password])
self.rpc('createwallet', ['wallet.dat', False, True, '', False, False])
self.rpc_wallet('encryptwallet', [password])
# Max timeout value, ~3 years
self.rpc_callback('walletpassphrase', [password, 100000000])
self.rpc_wallet('walletpassphrase', [password, 100000000])
self._sc.checkWalletSeed(self.coin_type())
def lockWallet(self):
self._log.info('lockWallet - {}'.format(self.ticker()))
self.rpc_callback('walletlock')
self.rpc_wallet('walletlock')
def get_p2sh_script_pubkey(self, script: bytearray) -> bytearray:
script_hash = hash160(script)
@@ -1449,7 +1508,7 @@ class BTCInterface(CoinInterface):
def findTxnByHash(self, txid_hex: str):
# Only works for wallet txns
try:
rv = self.rpc_callback('gettransaction', [txid_hex])
rv = self.rpc_wallet('gettransaction', [txid_hex])
except Exception as ex:
self._log.debug('findTxnByHash getrawtransaction failed: {}'.format(txid_hex))
return None

View File

@@ -0,0 +1,191 @@
"""
Copyright (c) 2011 Jeff Garzik
AuthServiceProxy has the following improvements over python-jsonrpc's
ServiceProxy class:
- HTTP connections persist for the life of the AuthServiceProxy object
(if server supports HTTP/1.1)
- sends protocol 'version', per JSON-RPC 1.1
- sends proper, incrementing 'id'
- sends Basic HTTP authentication headers
- parses all JSON numbers that look like floats as Decimal
- uses standard Python json lib
Previous copyright, from python-jsonrpc/jsonrpc/proxy.py:
Copyright (c) 2007 Jan-Klaas Kollhof
This file is part of jsonrpc.
jsonrpc is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation; either version 2.1 of the License, or
(at your option) any later version.
This software is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with this software; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
"""
try:
import http.client as httplib
except ImportError:
import httplib
import base64
import decimal
import json
import logging
import socket
try:
import urllib.parse as urlparse
except ImportError:
import urlparse
USER_AGENT = "AuthServiceProxy/0.1"
HTTP_TIMEOUT = 30
log = logging.getLogger("BitcoinRPC")
class JSONRPCException(Exception):
def __init__(self, rpc_error):
try:
errmsg = '%(message)s (%(code)i)' % rpc_error
except (KeyError, TypeError):
errmsg = ''
Exception.__init__(self, errmsg)
self.error = rpc_error
def EncodeDecimal(o):
if isinstance(o, decimal.Decimal):
return str(o)
raise TypeError(repr(o) + " is not JSON serializable")
class AuthServiceProxy(object):
__id_count = 0
# ensure_ascii: escape unicode as \uXXXX, passed to json.dumps
def __init__(self, service_url, service_name=None, timeout=HTTP_TIMEOUT, connection=None, ensure_ascii=True):
self.__service_url = service_url
self._service_name = service_name
self.ensure_ascii = ensure_ascii # can be toggled on the fly by tests
self.__url = urlparse.urlparse(service_url)
if self.__url.port is None:
port = 80
else:
port = self.__url.port
(user, passwd) = (self.__url.username, self.__url.password)
try:
user = user.encode('utf8')
except AttributeError:
pass
try:
passwd = passwd.encode('utf8')
except AttributeError:
pass
authpair = user + b':' + passwd
self.__auth_header = b'Basic ' + base64.b64encode(authpair)
if connection:
# Callables re-use the connection of the original proxy
self.__conn = connection
elif self.__url.scheme == 'https':
self.__conn = httplib.HTTPSConnection(self.__url.hostname, port,
timeout=timeout)
else:
self.__conn = httplib.HTTPConnection(self.__url.hostname, port,
timeout=timeout)
def __getattr__(self, name):
if name.startswith('__') and name.endswith('__'):
# Python internal stuff
raise AttributeError
if self._service_name is not None:
name = "%s.%s" % (self._service_name, name)
return AuthServiceProxy(self.__service_url, name, connection=self.__conn)
def _request(self, method, path, postdata):
'''
Do a HTTP request, with retry if we get disconnected (e.g. due to a timeout).
This is a workaround for https://bugs.python.org/issue3566 which is fixed in Python 3.5.
'''
headers = {'Host': self.__url.hostname,
'User-Agent': USER_AGENT,
'Authorization': self.__auth_header,
'Content-type': 'application/json'}
try:
self.__conn.request(method, path, postdata, headers)
return self._get_response()
except httplib.BadStatusLine as e:
if e.line == "''": # if connection was closed, try again
self.__conn.close()
self.__conn.request(method, path, postdata, headers)
return self._get_response()
else:
raise
except (BrokenPipeError,ConnectionResetError):
# Python 3.5+ raises BrokenPipeError instead of BadStatusLine when the connection was reset
# ConnectionResetError happens on FreeBSD with Python 3.4
self.__conn.close()
self.__conn.request(method, path, postdata, headers)
return self._get_response()
def __call__(self, *args, **argsn):
AuthServiceProxy.__id_count += 1
log.debug("-%s-> %s %s"%(AuthServiceProxy.__id_count, self._service_name,
json.dumps(args, default=EncodeDecimal, ensure_ascii=self.ensure_ascii)))
if args and argsn:
raise ValueError('Cannot handle both named and positional arguments')
postdata = json.dumps({'version': '1.1',
'method': self._service_name,
'params': args or argsn,
'id': AuthServiceProxy.__id_count}, default=EncodeDecimal, ensure_ascii=self.ensure_ascii)
response = self._request('POST', self.__url.path, postdata.encode('utf-8'))
if response['error'] is not None:
raise JSONRPCException(response['error'])
elif 'result' not in response:
raise JSONRPCException({
'code': -343, 'message': 'missing JSON-RPC result'})
else:
return response['result']
def _batch(self, rpc_call_list):
postdata = json.dumps(list(rpc_call_list), default=EncodeDecimal, ensure_ascii=self.ensure_ascii)
log.debug("--> "+postdata)
return self._request('POST', self.__url.path, postdata.encode('utf-8'))
def _get_response(self):
try:
http_response = self.__conn.getresponse()
except socket.timeout as e:
raise JSONRPCException({
'code': -344,
'message': '%r RPC took longer than %f seconds. Consider '
'using larger timeout for calls that take '
'longer to return.' % (self._service_name,
self.__conn.timeout)})
if http_response is None:
raise JSONRPCException({
'code': -342, 'message': 'missing HTTP response from server'})
content_type = http_response.getheader('Content-Type')
if content_type != 'application/json':
raise JSONRPCException({
'code': -342, 'message': 'non-JSON HTTP response with \'%i %s\' from server' % (http_response.status, http_response.reason)})
responsedata = http_response.read().decode('utf8')
response = json.loads(responsedata, parse_float=decimal.Decimal)
if "error" in response and response["error"] is None:
log.debug("<-%s- %s"%(response["id"], json.dumps(response["result"], default=EncodeDecimal, ensure_ascii=self.ensure_ascii)))
else:
log.debug("<-- "+responsedata)
return response

View File

@@ -0,0 +1,101 @@
#!/usr/bin/env python3
#
# bignum.py
#
# This file is copied from python-bitcoinlib.
#
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
#
"""Bignum routines"""
import struct
# generic big endian MPI format
def bn_bytes(v, have_ext=False):
ext = 0
if have_ext:
ext = 1
return ((v.bit_length()+7)//8) + ext
def bn2bin(v):
s = bytearray()
i = bn_bytes(v)
while i > 0:
s.append((v >> ((i-1) * 8)) & 0xff)
i -= 1
return s
def bin2bn(s):
l = 0
for ch in s:
l = (l << 8) | ch
return l
def bn2mpi(v):
have_ext = False
if v.bit_length() > 0:
have_ext = (v.bit_length() & 0x07) == 0
neg = False
if v < 0:
neg = True
v = -v
s = struct.pack(b">I", bn_bytes(v, have_ext))
ext = bytearray()
if have_ext:
ext.append(0)
v_bin = bn2bin(v)
if neg:
if have_ext:
ext[0] |= 0x80
else:
v_bin[0] |= 0x80
return s + ext + v_bin
def mpi2bn(s):
if len(s) < 4:
return None
s_size = bytes(s[:4])
v_len = struct.unpack(b">I", s_size)[0]
if len(s) != (v_len + 4):
return None
if v_len == 0:
return 0
v_str = bytearray(s[4:])
neg = False
i = v_str[0]
if i & 0x80:
neg = True
i &= ~0x80
v_str[0] = i
v = bin2bn(v_str)
if neg:
return -v
return v
# bitcoin-specific little endian format, with implicit size
def mpi2vch(s):
r = s[4:] # strip size
r = r[::-1] # reverse string, converting BE->LE
return r
def bn2vch(v):
return bytes(mpi2vch(bn2mpi(v)))
def vch2mpi(s):
r = struct.pack(b">I", len(s)) # size
r += s[::-1] # reverse string, converting LE->BE
return r
def vch2bn(s):
return mpi2bn(vch2mpi(s))

View File

@@ -0,0 +1,106 @@
#!/usr/bin/env python3
# Copyright (c) 2015-2016 The Bitcoin Core developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
"""
This module contains utilities for doing coverage analysis on the RPC
interface.
It provides a way to track which RPC commands are exercised during
testing.
"""
import os
REFERENCE_FILENAME = 'rpc_interface.txt'
class AuthServiceProxyWrapper(object):
"""
An object that wraps AuthServiceProxy to record specific RPC calls.
"""
def __init__(self, auth_service_proxy_instance, coverage_logfile=None):
"""
Kwargs:
auth_service_proxy_instance (AuthServiceProxy): the instance
being wrapped.
coverage_logfile (str): if specified, write each service_name
out to a file when called.
"""
self.auth_service_proxy_instance = auth_service_proxy_instance
self.coverage_logfile = coverage_logfile
def __getattr__(self, *args, **kwargs):
return_val = self.auth_service_proxy_instance.__getattr__(
*args, **kwargs)
return AuthServiceProxyWrapper(return_val, self.coverage_logfile)
def __call__(self, *args, **kwargs):
"""
Delegates to AuthServiceProxy, then writes the particular RPC method
called to a file.
"""
return_val = self.auth_service_proxy_instance.__call__(*args, **kwargs)
rpc_method = self.auth_service_proxy_instance._service_name
if self.coverage_logfile:
with open(self.coverage_logfile, 'a+', encoding='utf8') as f:
f.write("%s\n" % rpc_method)
return return_val
@property
def url(self):
return self.auth_service_proxy_instance.url
def get_filename(dirname, n_node):
"""
Get a filename unique to the test process ID and node.
This file will contain a list of RPC commands covered.
"""
pid = str(os.getpid())
return os.path.join(
dirname, "coverage.pid%s.node%s.txt" % (pid, str(n_node)))
def write_all_rpc_commands(dirname, node):
"""
Write out a list of all RPC functions available in `bitcoin-cli` for
coverage comparison. This will only happen once per coverage
directory.
Args:
dirname (str): temporary test dir
node (AuthServiceProxy): client
Returns:
bool. if the RPC interface file was written.
"""
filename = os.path.join(dirname, REFERENCE_FILENAME)
if os.path.isfile(filename):
return False
help_output = node.help().split('\n')
commands = set()
for line in help_output:
line = line.strip()
# Ignore blanks and headers
if line and not line.startswith('='):
commands.add("%s\n" % line.split()[0])
with open(filename, 'w', encoding='utf8') as f:
f.writelines(list(commands))
return True

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,943 @@
#!/usr/bin/env python3
# Copyright (c) 2015-2016 The Bitcoin Core developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
#
# script.py
#
# This file is modified from python-bitcoinlib.
#
"""Scripts
Functionality to build scripts, as well as SignatureHash().
"""
from .mininode import CTransaction, CTxOut, sha256, hash256, uint256_from_str, ser_uint256, ser_string
from binascii import hexlify
import hashlib
import sys
bchr = chr
bord = ord
if sys.version > '3':
long = int
bchr = lambda x: bytes([x])
bord = lambda x: x
import struct
from .bignum import bn2vch
MAX_SCRIPT_SIZE = 10000
MAX_SCRIPT_ELEMENT_SIZE = 520
MAX_SCRIPT_OPCODES = 201
OPCODE_NAMES = {}
_opcode_instances = []
class CScriptOp(int):
"""A single script opcode"""
__slots__ = []
@staticmethod
def encode_op_pushdata(d):
"""Encode a PUSHDATA op, returning bytes"""
if len(d) < 0x4c:
return b'' + bchr(len(d)) + d # OP_PUSHDATA
elif len(d) <= 0xff:
return b'\x4c' + bchr(len(d)) + d # OP_PUSHDATA1
elif len(d) <= 0xffff:
return b'\x4d' + struct.pack(b'<H', len(d)) + d # OP_PUSHDATA2
elif len(d) <= 0xffffffff:
return b'\x4e' + struct.pack(b'<I', len(d)) + d # OP_PUSHDATA4
else:
raise ValueError("Data too long to encode in a PUSHDATA op")
@staticmethod
def encode_op_n(n):
"""Encode a small integer op, returning an opcode"""
if not (0 <= n <= 16):
raise ValueError('Integer must be in range 0 <= n <= 16, got %d' % n)
if n == 0:
return OP_0
else:
return CScriptOp(OP_1 + n-1)
def decode_op_n(self):
"""Decode a small integer opcode, returning an integer"""
if self == OP_0:
return 0
if not (self == OP_0 or OP_1 <= self <= OP_16):
raise ValueError('op %r is not an OP_N' % self)
return int(self - OP_1+1)
def is_small_int(self):
"""Return true if the op pushes a small integer to the stack"""
if 0x51 <= self <= 0x60 or self == 0:
return True
else:
return False
def __str__(self):
return repr(self)
def __repr__(self):
if self in OPCODE_NAMES:
return OPCODE_NAMES[self]
else:
return 'CScriptOp(0x%x)' % self
def __new__(cls, n):
try:
return _opcode_instances[n]
except IndexError:
assert len(_opcode_instances) == n
_opcode_instances.append(super(CScriptOp, cls).__new__(cls, n))
return _opcode_instances[n]
# Populate opcode instance table
for n in range(0xff+1):
CScriptOp(n)
# push value
OP_0 = CScriptOp(0x00)
OP_FALSE = OP_0
OP_PUSHDATA1 = CScriptOp(0x4c)
OP_PUSHDATA2 = CScriptOp(0x4d)
OP_PUSHDATA4 = CScriptOp(0x4e)
OP_1NEGATE = CScriptOp(0x4f)
OP_RESERVED = CScriptOp(0x50)
OP_1 = CScriptOp(0x51)
OP_TRUE=OP_1
OP_2 = CScriptOp(0x52)
OP_3 = CScriptOp(0x53)
OP_4 = CScriptOp(0x54)
OP_5 = CScriptOp(0x55)
OP_6 = CScriptOp(0x56)
OP_7 = CScriptOp(0x57)
OP_8 = CScriptOp(0x58)
OP_9 = CScriptOp(0x59)
OP_10 = CScriptOp(0x5a)
OP_11 = CScriptOp(0x5b)
OP_12 = CScriptOp(0x5c)
OP_13 = CScriptOp(0x5d)
OP_14 = CScriptOp(0x5e)
OP_15 = CScriptOp(0x5f)
OP_16 = CScriptOp(0x60)
# control
OP_NOP = CScriptOp(0x61)
OP_VER = CScriptOp(0x62)
OP_IF = CScriptOp(0x63)
OP_NOTIF = CScriptOp(0x64)
OP_VERIF = CScriptOp(0x65)
OP_VERNOTIF = CScriptOp(0x66)
OP_ELSE = CScriptOp(0x67)
OP_ENDIF = CScriptOp(0x68)
OP_VERIFY = CScriptOp(0x69)
OP_RETURN = CScriptOp(0x6a)
# stack ops
OP_TOALTSTACK = CScriptOp(0x6b)
OP_FROMALTSTACK = CScriptOp(0x6c)
OP_2DROP = CScriptOp(0x6d)
OP_2DUP = CScriptOp(0x6e)
OP_3DUP = CScriptOp(0x6f)
OP_2OVER = CScriptOp(0x70)
OP_2ROT = CScriptOp(0x71)
OP_2SWAP = CScriptOp(0x72)
OP_IFDUP = CScriptOp(0x73)
OP_DEPTH = CScriptOp(0x74)
OP_DROP = CScriptOp(0x75)
OP_DUP = CScriptOp(0x76)
OP_NIP = CScriptOp(0x77)
OP_OVER = CScriptOp(0x78)
OP_PICK = CScriptOp(0x79)
OP_ROLL = CScriptOp(0x7a)
OP_ROT = CScriptOp(0x7b)
OP_SWAP = CScriptOp(0x7c)
OP_TUCK = CScriptOp(0x7d)
# splice ops
OP_CAT = CScriptOp(0x7e)
OP_SUBSTR = CScriptOp(0x7f)
OP_LEFT = CScriptOp(0x80)
OP_RIGHT = CScriptOp(0x81)
OP_SIZE = CScriptOp(0x82)
# bit logic
OP_INVERT = CScriptOp(0x83)
OP_AND = CScriptOp(0x84)
OP_OR = CScriptOp(0x85)
OP_XOR = CScriptOp(0x86)
OP_EQUAL = CScriptOp(0x87)
OP_EQUALVERIFY = CScriptOp(0x88)
OP_RESERVED1 = CScriptOp(0x89)
OP_RESERVED2 = CScriptOp(0x8a)
# numeric
OP_1ADD = CScriptOp(0x8b)
OP_1SUB = CScriptOp(0x8c)
OP_2MUL = CScriptOp(0x8d)
OP_2DIV = CScriptOp(0x8e)
OP_NEGATE = CScriptOp(0x8f)
OP_ABS = CScriptOp(0x90)
OP_NOT = CScriptOp(0x91)
OP_0NOTEQUAL = CScriptOp(0x92)
OP_ADD = CScriptOp(0x93)
OP_SUB = CScriptOp(0x94)
OP_MUL = CScriptOp(0x95)
OP_DIV = CScriptOp(0x96)
OP_MOD = CScriptOp(0x97)
OP_LSHIFT = CScriptOp(0x98)
OP_RSHIFT = CScriptOp(0x99)
OP_BOOLAND = CScriptOp(0x9a)
OP_BOOLOR = CScriptOp(0x9b)
OP_NUMEQUAL = CScriptOp(0x9c)
OP_NUMEQUALVERIFY = CScriptOp(0x9d)
OP_NUMNOTEQUAL = CScriptOp(0x9e)
OP_LESSTHAN = CScriptOp(0x9f)
OP_GREATERTHAN = CScriptOp(0xa0)
OP_LESSTHANOREQUAL = CScriptOp(0xa1)
OP_GREATERTHANOREQUAL = CScriptOp(0xa2)
OP_MIN = CScriptOp(0xa3)
OP_MAX = CScriptOp(0xa4)
OP_WITHIN = CScriptOp(0xa5)
# crypto
OP_RIPEMD160 = CScriptOp(0xa6)
OP_SHA1 = CScriptOp(0xa7)
OP_SHA256 = CScriptOp(0xa8)
OP_HASH160 = CScriptOp(0xa9)
OP_HASH256 = CScriptOp(0xaa)
OP_CODESEPARATOR = CScriptOp(0xab)
OP_CHECKSIG = CScriptOp(0xac)
OP_CHECKSIGVERIFY = CScriptOp(0xad)
OP_CHECKMULTISIG = CScriptOp(0xae)
OP_CHECKMULTISIGVERIFY = CScriptOp(0xaf)
# expansion
OP_NOP1 = CScriptOp(0xb0)
OP_CHECKLOCKTIMEVERIFY = CScriptOp(0xb1)
OP_CHECKSEQUENCEVERIFY = CScriptOp(0xb2)
OP_NOP4 = CScriptOp(0xb3)
OP_NOP5 = CScriptOp(0xb4)
OP_NOP6 = CScriptOp(0xb5)
OP_NOP7 = CScriptOp(0xb6)
OP_NOP8 = CScriptOp(0xb7)
OP_NOP9 = CScriptOp(0xb8)
OP_NOP10 = CScriptOp(0xb9)
# template matching params
OP_SMALLINTEGER = CScriptOp(0xfa)
OP_PUBKEYS = CScriptOp(0xfb)
OP_PUBKEYHASH = CScriptOp(0xfd)
OP_PUBKEY = CScriptOp(0xfe)
OP_INVALIDOPCODE = CScriptOp(0xff)
VALID_OPCODES = {
OP_1NEGATE,
OP_RESERVED,
OP_1,
OP_2,
OP_3,
OP_4,
OP_5,
OP_6,
OP_7,
OP_8,
OP_9,
OP_10,
OP_11,
OP_12,
OP_13,
OP_14,
OP_15,
OP_16,
OP_NOP,
OP_VER,
OP_IF,
OP_NOTIF,
OP_VERIF,
OP_VERNOTIF,
OP_ELSE,
OP_ENDIF,
OP_VERIFY,
OP_RETURN,
OP_TOALTSTACK,
OP_FROMALTSTACK,
OP_2DROP,
OP_2DUP,
OP_3DUP,
OP_2OVER,
OP_2ROT,
OP_2SWAP,
OP_IFDUP,
OP_DEPTH,
OP_DROP,
OP_DUP,
OP_NIP,
OP_OVER,
OP_PICK,
OP_ROLL,
OP_ROT,
OP_SWAP,
OP_TUCK,
OP_CAT,
OP_SUBSTR,
OP_LEFT,
OP_RIGHT,
OP_SIZE,
OP_INVERT,
OP_AND,
OP_OR,
OP_XOR,
OP_EQUAL,
OP_EQUALVERIFY,
OP_RESERVED1,
OP_RESERVED2,
OP_1ADD,
OP_1SUB,
OP_2MUL,
OP_2DIV,
OP_NEGATE,
OP_ABS,
OP_NOT,
OP_0NOTEQUAL,
OP_ADD,
OP_SUB,
OP_MUL,
OP_DIV,
OP_MOD,
OP_LSHIFT,
OP_RSHIFT,
OP_BOOLAND,
OP_BOOLOR,
OP_NUMEQUAL,
OP_NUMEQUALVERIFY,
OP_NUMNOTEQUAL,
OP_LESSTHAN,
OP_GREATERTHAN,
OP_LESSTHANOREQUAL,
OP_GREATERTHANOREQUAL,
OP_MIN,
OP_MAX,
OP_WITHIN,
OP_RIPEMD160,
OP_SHA1,
OP_SHA256,
OP_HASH160,
OP_HASH256,
OP_CODESEPARATOR,
OP_CHECKSIG,
OP_CHECKSIGVERIFY,
OP_CHECKMULTISIG,
OP_CHECKMULTISIGVERIFY,
OP_NOP1,
OP_CHECKLOCKTIMEVERIFY,
OP_CHECKSEQUENCEVERIFY,
OP_NOP4,
OP_NOP5,
OP_NOP6,
OP_NOP7,
OP_NOP8,
OP_NOP9,
OP_NOP10,
OP_SMALLINTEGER,
OP_PUBKEYS,
OP_PUBKEYHASH,
OP_PUBKEY,
}
OPCODE_NAMES.update({
OP_0 : 'OP_0',
OP_PUSHDATA1 : 'OP_PUSHDATA1',
OP_PUSHDATA2 : 'OP_PUSHDATA2',
OP_PUSHDATA4 : 'OP_PUSHDATA4',
OP_1NEGATE : 'OP_1NEGATE',
OP_RESERVED : 'OP_RESERVED',
OP_1 : 'OP_1',
OP_2 : 'OP_2',
OP_3 : 'OP_3',
OP_4 : 'OP_4',
OP_5 : 'OP_5',
OP_6 : 'OP_6',
OP_7 : 'OP_7',
OP_8 : 'OP_8',
OP_9 : 'OP_9',
OP_10 : 'OP_10',
OP_11 : 'OP_11',
OP_12 : 'OP_12',
OP_13 : 'OP_13',
OP_14 : 'OP_14',
OP_15 : 'OP_15',
OP_16 : 'OP_16',
OP_NOP : 'OP_NOP',
OP_VER : 'OP_VER',
OP_IF : 'OP_IF',
OP_NOTIF : 'OP_NOTIF',
OP_VERIF : 'OP_VERIF',
OP_VERNOTIF : 'OP_VERNOTIF',
OP_ELSE : 'OP_ELSE',
OP_ENDIF : 'OP_ENDIF',
OP_VERIFY : 'OP_VERIFY',
OP_RETURN : 'OP_RETURN',
OP_TOALTSTACK : 'OP_TOALTSTACK',
OP_FROMALTSTACK : 'OP_FROMALTSTACK',
OP_2DROP : 'OP_2DROP',
OP_2DUP : 'OP_2DUP',
OP_3DUP : 'OP_3DUP',
OP_2OVER : 'OP_2OVER',
OP_2ROT : 'OP_2ROT',
OP_2SWAP : 'OP_2SWAP',
OP_IFDUP : 'OP_IFDUP',
OP_DEPTH : 'OP_DEPTH',
OP_DROP : 'OP_DROP',
OP_DUP : 'OP_DUP',
OP_NIP : 'OP_NIP',
OP_OVER : 'OP_OVER',
OP_PICK : 'OP_PICK',
OP_ROLL : 'OP_ROLL',
OP_ROT : 'OP_ROT',
OP_SWAP : 'OP_SWAP',
OP_TUCK : 'OP_TUCK',
OP_CAT : 'OP_CAT',
OP_SUBSTR : 'OP_SUBSTR',
OP_LEFT : 'OP_LEFT',
OP_RIGHT : 'OP_RIGHT',
OP_SIZE : 'OP_SIZE',
OP_INVERT : 'OP_INVERT',
OP_AND : 'OP_AND',
OP_OR : 'OP_OR',
OP_XOR : 'OP_XOR',
OP_EQUAL : 'OP_EQUAL',
OP_EQUALVERIFY : 'OP_EQUALVERIFY',
OP_RESERVED1 : 'OP_RESERVED1',
OP_RESERVED2 : 'OP_RESERVED2',
OP_1ADD : 'OP_1ADD',
OP_1SUB : 'OP_1SUB',
OP_2MUL : 'OP_2MUL',
OP_2DIV : 'OP_2DIV',
OP_NEGATE : 'OP_NEGATE',
OP_ABS : 'OP_ABS',
OP_NOT : 'OP_NOT',
OP_0NOTEQUAL : 'OP_0NOTEQUAL',
OP_ADD : 'OP_ADD',
OP_SUB : 'OP_SUB',
OP_MUL : 'OP_MUL',
OP_DIV : 'OP_DIV',
OP_MOD : 'OP_MOD',
OP_LSHIFT : 'OP_LSHIFT',
OP_RSHIFT : 'OP_RSHIFT',
OP_BOOLAND : 'OP_BOOLAND',
OP_BOOLOR : 'OP_BOOLOR',
OP_NUMEQUAL : 'OP_NUMEQUAL',
OP_NUMEQUALVERIFY : 'OP_NUMEQUALVERIFY',
OP_NUMNOTEQUAL : 'OP_NUMNOTEQUAL',
OP_LESSTHAN : 'OP_LESSTHAN',
OP_GREATERTHAN : 'OP_GREATERTHAN',
OP_LESSTHANOREQUAL : 'OP_LESSTHANOREQUAL',
OP_GREATERTHANOREQUAL : 'OP_GREATERTHANOREQUAL',
OP_MIN : 'OP_MIN',
OP_MAX : 'OP_MAX',
OP_WITHIN : 'OP_WITHIN',
OP_RIPEMD160 : 'OP_RIPEMD160',
OP_SHA1 : 'OP_SHA1',
OP_SHA256 : 'OP_SHA256',
OP_HASH160 : 'OP_HASH160',
OP_HASH256 : 'OP_HASH256',
OP_CODESEPARATOR : 'OP_CODESEPARATOR',
OP_CHECKSIG : 'OP_CHECKSIG',
OP_CHECKSIGVERIFY : 'OP_CHECKSIGVERIFY',
OP_CHECKMULTISIG : 'OP_CHECKMULTISIG',
OP_CHECKMULTISIGVERIFY : 'OP_CHECKMULTISIGVERIFY',
OP_NOP1 : 'OP_NOP1',
OP_CHECKLOCKTIMEVERIFY : 'OP_CHECKLOCKTIMEVERIFY',
OP_CHECKSEQUENCEVERIFY : 'OP_CHECKSEQUENCEVERIFY',
OP_NOP4 : 'OP_NOP4',
OP_NOP5 : 'OP_NOP5',
OP_NOP6 : 'OP_NOP6',
OP_NOP7 : 'OP_NOP7',
OP_NOP8 : 'OP_NOP8',
OP_NOP9 : 'OP_NOP9',
OP_NOP10 : 'OP_NOP10',
OP_SMALLINTEGER : 'OP_SMALLINTEGER',
OP_PUBKEYS : 'OP_PUBKEYS',
OP_PUBKEYHASH : 'OP_PUBKEYHASH',
OP_PUBKEY : 'OP_PUBKEY',
OP_INVALIDOPCODE : 'OP_INVALIDOPCODE',
})
OPCODES_BY_NAME = {
'OP_0' : OP_0,
'OP_PUSHDATA1' : OP_PUSHDATA1,
'OP_PUSHDATA2' : OP_PUSHDATA2,
'OP_PUSHDATA4' : OP_PUSHDATA4,
'OP_1NEGATE' : OP_1NEGATE,
'OP_RESERVED' : OP_RESERVED,
'OP_1' : OP_1,
'OP_2' : OP_2,
'OP_3' : OP_3,
'OP_4' : OP_4,
'OP_5' : OP_5,
'OP_6' : OP_6,
'OP_7' : OP_7,
'OP_8' : OP_8,
'OP_9' : OP_9,
'OP_10' : OP_10,
'OP_11' : OP_11,
'OP_12' : OP_12,
'OP_13' : OP_13,
'OP_14' : OP_14,
'OP_15' : OP_15,
'OP_16' : OP_16,
'OP_NOP' : OP_NOP,
'OP_VER' : OP_VER,
'OP_IF' : OP_IF,
'OP_NOTIF' : OP_NOTIF,
'OP_VERIF' : OP_VERIF,
'OP_VERNOTIF' : OP_VERNOTIF,
'OP_ELSE' : OP_ELSE,
'OP_ENDIF' : OP_ENDIF,
'OP_VERIFY' : OP_VERIFY,
'OP_RETURN' : OP_RETURN,
'OP_TOALTSTACK' : OP_TOALTSTACK,
'OP_FROMALTSTACK' : OP_FROMALTSTACK,
'OP_2DROP' : OP_2DROP,
'OP_2DUP' : OP_2DUP,
'OP_3DUP' : OP_3DUP,
'OP_2OVER' : OP_2OVER,
'OP_2ROT' : OP_2ROT,
'OP_2SWAP' : OP_2SWAP,
'OP_IFDUP' : OP_IFDUP,
'OP_DEPTH' : OP_DEPTH,
'OP_DROP' : OP_DROP,
'OP_DUP' : OP_DUP,
'OP_NIP' : OP_NIP,
'OP_OVER' : OP_OVER,
'OP_PICK' : OP_PICK,
'OP_ROLL' : OP_ROLL,
'OP_ROT' : OP_ROT,
'OP_SWAP' : OP_SWAP,
'OP_TUCK' : OP_TUCK,
'OP_CAT' : OP_CAT,
'OP_SUBSTR' : OP_SUBSTR,
'OP_LEFT' : OP_LEFT,
'OP_RIGHT' : OP_RIGHT,
'OP_SIZE' : OP_SIZE,
'OP_INVERT' : OP_INVERT,
'OP_AND' : OP_AND,
'OP_OR' : OP_OR,
'OP_XOR' : OP_XOR,
'OP_EQUAL' : OP_EQUAL,
'OP_EQUALVERIFY' : OP_EQUALVERIFY,
'OP_RESERVED1' : OP_RESERVED1,
'OP_RESERVED2' : OP_RESERVED2,
'OP_1ADD' : OP_1ADD,
'OP_1SUB' : OP_1SUB,
'OP_2MUL' : OP_2MUL,
'OP_2DIV' : OP_2DIV,
'OP_NEGATE' : OP_NEGATE,
'OP_ABS' : OP_ABS,
'OP_NOT' : OP_NOT,
'OP_0NOTEQUAL' : OP_0NOTEQUAL,
'OP_ADD' : OP_ADD,
'OP_SUB' : OP_SUB,
'OP_MUL' : OP_MUL,
'OP_DIV' : OP_DIV,
'OP_MOD' : OP_MOD,
'OP_LSHIFT' : OP_LSHIFT,
'OP_RSHIFT' : OP_RSHIFT,
'OP_BOOLAND' : OP_BOOLAND,
'OP_BOOLOR' : OP_BOOLOR,
'OP_NUMEQUAL' : OP_NUMEQUAL,
'OP_NUMEQUALVERIFY' : OP_NUMEQUALVERIFY,
'OP_NUMNOTEQUAL' : OP_NUMNOTEQUAL,
'OP_LESSTHAN' : OP_LESSTHAN,
'OP_GREATERTHAN' : OP_GREATERTHAN,
'OP_LESSTHANOREQUAL' : OP_LESSTHANOREQUAL,
'OP_GREATERTHANOREQUAL' : OP_GREATERTHANOREQUAL,
'OP_MIN' : OP_MIN,
'OP_MAX' : OP_MAX,
'OP_WITHIN' : OP_WITHIN,
'OP_RIPEMD160' : OP_RIPEMD160,
'OP_SHA1' : OP_SHA1,
'OP_SHA256' : OP_SHA256,
'OP_HASH160' : OP_HASH160,
'OP_HASH256' : OP_HASH256,
'OP_CODESEPARATOR' : OP_CODESEPARATOR,
'OP_CHECKSIG' : OP_CHECKSIG,
'OP_CHECKSIGVERIFY' : OP_CHECKSIGVERIFY,
'OP_CHECKMULTISIG' : OP_CHECKMULTISIG,
'OP_CHECKMULTISIGVERIFY' : OP_CHECKMULTISIGVERIFY,
'OP_NOP1' : OP_NOP1,
'OP_CHECKLOCKTIMEVERIFY' : OP_CHECKLOCKTIMEVERIFY,
'OP_CHECKSEQUENCEVERIFY' : OP_CHECKSEQUENCEVERIFY,
'OP_NOP4' : OP_NOP4,
'OP_NOP5' : OP_NOP5,
'OP_NOP6' : OP_NOP6,
'OP_NOP7' : OP_NOP7,
'OP_NOP8' : OP_NOP8,
'OP_NOP9' : OP_NOP9,
'OP_NOP10' : OP_NOP10,
'OP_SMALLINTEGER' : OP_SMALLINTEGER,
'OP_PUBKEYS' : OP_PUBKEYS,
'OP_PUBKEYHASH' : OP_PUBKEYHASH,
'OP_PUBKEY' : OP_PUBKEY,
}
class CScriptInvalidError(Exception):
"""Base class for CScript exceptions"""
pass
class CScriptTruncatedPushDataError(CScriptInvalidError):
"""Invalid pushdata due to truncation"""
def __init__(self, msg, data):
self.data = data
super(CScriptTruncatedPushDataError, self).__init__(msg)
# This is used, eg, for blockchain heights in coinbase scripts (bip34)
class CScriptNum(object):
def __init__(self, d=0):
self.value = d
@staticmethod
def encode(obj):
r = bytearray(0)
if obj.value == 0:
return bytes(r)
neg = obj.value < 0
absvalue = -obj.value if neg else obj.value
while (absvalue):
r.append(absvalue & 0xff)
absvalue >>= 8
if r[-1] & 0x80:
r.append(0x80 if neg else 0)
elif neg:
r[-1] |= 0x80
return bytes(bchr(len(r)) + r)
class CScript(bytes):
"""Serialized script
A bytes subclass, so you can use this directly whenever bytes are accepted.
Note that this means that indexing does *not* work - you'll get an index by
byte rather than opcode. This format was chosen for efficiency so that the
general case would not require creating a lot of little CScriptOP objects.
iter(script) however does iterate by opcode.
"""
@classmethod
def __coerce_instance(cls, other):
# Coerce other into bytes
if isinstance(other, CScriptOp):
other = bchr(other)
elif isinstance(other, CScriptNum):
if (other.value == 0):
other = bchr(CScriptOp(OP_0))
else:
other = CScriptNum.encode(other)
elif isinstance(other, int):
if 0 <= other <= 16:
other = bytes(bchr(CScriptOp.encode_op_n(other)))
elif other == -1:
other = bytes(bchr(OP_1NEGATE))
else:
other = CScriptOp.encode_op_pushdata(bn2vch(other))
elif isinstance(other, (bytes, bytearray)):
other = CScriptOp.encode_op_pushdata(other)
return other
def __add__(self, other):
# Do the coercion outside of the try block so that errors in it are
# noticed.
other = self.__coerce_instance(other)
try:
# bytes.__add__ always returns bytes instances unfortunately
return CScript(super(CScript, self).__add__(other))
except TypeError:
raise TypeError('Can not add a %r instance to a CScript' % other.__class__)
def join(self, iterable):
# join makes no sense for a CScript()
raise NotImplementedError
def __new__(cls, value=b''):
if isinstance(value, bytes) or isinstance(value, bytearray):
return super(CScript, cls).__new__(cls, value)
else:
def coerce_iterable(iterable):
for instance in iterable:
yield cls.__coerce_instance(instance)
# Annoyingly on both python2 and python3 bytes.join() always
# returns a bytes instance even when subclassed.
return super(CScript, cls).__new__(cls, b''.join(coerce_iterable(value)))
def raw_iter(self):
"""Raw iteration
Yields tuples of (opcode, data, sop_idx) so that the different possible
PUSHDATA encodings can be accurately distinguished, as well as
determining the exact opcode byte indexes. (sop_idx)
"""
i = 0
while i < len(self):
sop_idx = i
opcode = bord(self[i])
i += 1
if opcode > OP_PUSHDATA4:
yield (opcode, None, sop_idx)
else:
datasize = None
pushdata_type = None
if opcode < OP_PUSHDATA1:
pushdata_type = 'PUSHDATA(%d)' % opcode
datasize = opcode
elif opcode == OP_PUSHDATA1:
pushdata_type = 'PUSHDATA1'
if i >= len(self):
raise CScriptInvalidError('PUSHDATA1: missing data length')
datasize = bord(self[i])
i += 1
elif opcode == OP_PUSHDATA2:
pushdata_type = 'PUSHDATA2'
if i + 1 >= len(self):
raise CScriptInvalidError('PUSHDATA2: missing data length')
datasize = bord(self[i]) + (bord(self[i+1]) << 8)
i += 2
elif opcode == OP_PUSHDATA4:
pushdata_type = 'PUSHDATA4'
if i + 3 >= len(self):
raise CScriptInvalidError('PUSHDATA4: missing data length')
datasize = bord(self[i]) + (bord(self[i+1]) << 8) + (bord(self[i+2]) << 16) + (bord(self[i+3]) << 24)
i += 4
else:
assert False # shouldn't happen
data = bytes(self[i:i+datasize])
# Check for truncation
if len(data) < datasize:
raise CScriptTruncatedPushDataError('%s: truncated data' % pushdata_type, data)
i += datasize
yield (opcode, data, sop_idx)
def __iter__(self):
"""'Cooked' iteration
Returns either a CScriptOP instance, an integer, or bytes, as
appropriate.
See raw_iter() if you need to distinguish the different possible
PUSHDATA encodings.
"""
for (opcode, data, sop_idx) in self.raw_iter():
if data is not None:
yield data
else:
opcode = CScriptOp(opcode)
if opcode.is_small_int():
yield opcode.decode_op_n()
else:
yield CScriptOp(opcode)
def __repr__(self):
# For Python3 compatibility add b before strings so testcases don't
# need to change
def _repr(o):
if isinstance(o, bytes):
return b"x('%s')" % hexlify(o).decode('ascii')
else:
return repr(o)
ops = []
i = iter(self)
while True:
op = None
try:
op = _repr(next(i))
except CScriptTruncatedPushDataError as err:
op = '%s...<ERROR: %s>' % (_repr(err.data), err)
break
except CScriptInvalidError as err:
op = '<ERROR: %s>' % err
break
except StopIteration:
break
finally:
if op is not None:
ops.append(op)
return "CScript([%s])" % ', '.join(ops)
def GetSigOpCount(self, fAccurate):
"""Get the SigOp count.
fAccurate - Accurately count CHECKMULTISIG, see BIP16 for details.
Note that this is consensus-critical.
"""
n = 0
lastOpcode = OP_INVALIDOPCODE
for (opcode, data, sop_idx) in self.raw_iter():
if opcode in (OP_CHECKSIG, OP_CHECKSIGVERIFY):
n += 1
elif opcode in (OP_CHECKMULTISIG, OP_CHECKMULTISIGVERIFY):
if fAccurate and (OP_1 <= lastOpcode <= OP_16):
n += opcode.decode_op_n()
else:
n += 20
lastOpcode = opcode
return n
SIGHASH_ALL = 1
SIGHASH_NONE = 2
SIGHASH_SINGLE = 3
SIGHASH_ANYONECANPAY = 0x80
def FindAndDelete(script, sig):
"""Consensus critical, see FindAndDelete() in Satoshi codebase"""
r = b''
last_sop_idx = sop_idx = 0
skip = True
for (opcode, data, sop_idx) in script.raw_iter():
if not skip:
r += script[last_sop_idx:sop_idx]
last_sop_idx = sop_idx
if script[sop_idx:sop_idx + len(sig)] == sig:
skip = True
else:
skip = False
if not skip:
r += script[last_sop_idx:]
return CScript(r)
def SignatureHash(script, txTo, inIdx, hashtype):
"""Consensus-correct SignatureHash
Returns (hash, err) to precisely match the consensus-critical behavior of
the SIGHASH_SINGLE bug. (inIdx is *not* checked for validity)
"""
HASH_ONE = b'\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
if inIdx >= len(txTo.vin):
return (HASH_ONE, "inIdx %d out of range (%d)" % (inIdx, len(txTo.vin)))
txtmp = CTransaction(txTo)
for txin in txtmp.vin:
txin.scriptSig = b''
txtmp.vin[inIdx].scriptSig = FindAndDelete(script, CScript([OP_CODESEPARATOR]))
if (hashtype & 0x1f) == SIGHASH_NONE:
txtmp.vout = []
for i in range(len(txtmp.vin)):
if i != inIdx:
txtmp.vin[i].nSequence = 0
elif (hashtype & 0x1f) == SIGHASH_SINGLE:
outIdx = inIdx
if outIdx >= len(txtmp.vout):
return (HASH_ONE, "outIdx %d out of range (%d)" % (outIdx, len(txtmp.vout)))
tmp = txtmp.vout[outIdx]
txtmp.vout = []
for i in range(outIdx):
txtmp.vout.append(CTxOut(-1))
txtmp.vout.append(tmp)
for i in range(len(txtmp.vin)):
if i != inIdx:
txtmp.vin[i].nSequence = 0
if hashtype & SIGHASH_ANYONECANPAY:
tmp = txtmp.vin[inIdx]
txtmp.vin = []
txtmp.vin.append(tmp)
s = txtmp.serialize()
s += struct.pack(b"<I", hashtype)
hash = hash256(s)
return (hash, None)
# TODO: Allow cached hashPrevouts/hashSequence/hashOutputs to be provided.
# Performance optimization probably not necessary for python tests, however.
# Note that this corresponds to sigversion == 1 in EvalScript, which is used
# for version 0 witnesses.
def SegwitVersion1SignatureHash(script, txTo, inIdx, hashtype, amount):
hashPrevouts = 0
hashSequence = 0
hashOutputs = 0
if not (hashtype & SIGHASH_ANYONECANPAY):
serialize_prevouts = bytes()
for i in txTo.vin:
serialize_prevouts += i.prevout.serialize()
hashPrevouts = uint256_from_str(hash256(serialize_prevouts))
if (not (hashtype & SIGHASH_ANYONECANPAY) and (hashtype & 0x1f) != SIGHASH_SINGLE and (hashtype & 0x1f) != SIGHASH_NONE):
serialize_sequence = bytes()
for i in txTo.vin:
serialize_sequence += struct.pack("<I", i.nSequence)
hashSequence = uint256_from_str(hash256(serialize_sequence))
if ((hashtype & 0x1f) != SIGHASH_SINGLE and (hashtype & 0x1f) != SIGHASH_NONE):
serialize_outputs = bytes()
for o in txTo.vout:
serialize_outputs += o.serialize()
hashOutputs = uint256_from_str(hash256(serialize_outputs))
elif ((hashtype & 0x1f) == SIGHASH_SINGLE and inIdx < len(txTo.vout)):
serialize_outputs = txTo.vout[inIdx].serialize()
hashOutputs = uint256_from_str(hash256(serialize_outputs))
ss = bytes()
ss += struct.pack("<i", txTo.nVersion)
ss += ser_uint256(hashPrevouts)
ss += ser_uint256(hashSequence)
ss += txTo.vin[inIdx].prevout.serialize()
ss += ser_string(script)
ss += struct.pack("<q", amount)
ss += struct.pack("<I", txTo.vin[inIdx].nSequence)
ss += ser_uint256(hashOutputs)
ss += struct.pack("<i", txTo.nLockTime)
ss += struct.pack("<I", hashtype)
return hash256(ss)

View File

@@ -0,0 +1,64 @@
#!/usr/bin/env python3
# Copyright (c) 2016 The Bitcoin Core developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
#
# siphash.py - Specialized SipHash-2-4 implementations
#
# This implements SipHash-2-4 for 256-bit integers.
def rotl64(n, b):
return n >> (64 - b) | (n & ((1 << (64 - b)) - 1)) << b
def siphash_round(v0, v1, v2, v3):
v0 = (v0 + v1) & ((1 << 64) - 1)
v1 = rotl64(v1, 13)
v1 ^= v0
v0 = rotl64(v0, 32)
v2 = (v2 + v3) & ((1 << 64) - 1)
v3 = rotl64(v3, 16)
v3 ^= v2
v0 = (v0 + v3) & ((1 << 64) - 1)
v3 = rotl64(v3, 21)
v3 ^= v0
v2 = (v2 + v1) & ((1 << 64) - 1)
v1 = rotl64(v1, 17)
v1 ^= v2
v2 = rotl64(v2, 32)
return (v0, v1, v2, v3)
def siphash256(k0, k1, h):
n0 = h & ((1 << 64) - 1)
n1 = (h >> 64) & ((1 << 64) - 1)
n2 = (h >> 128) & ((1 << 64) - 1)
n3 = (h >> 192) & ((1 << 64) - 1)
v0 = 0x736f6d6570736575 ^ k0
v1 = 0x646f72616e646f6d ^ k1
v2 = 0x6c7967656e657261 ^ k0
v3 = 0x7465646279746573 ^ k1 ^ n0
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
v0 ^= n0
v3 ^= n1
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
v0 ^= n1
v3 ^= n2
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
v0 ^= n2
v3 ^= n3
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
v0 ^= n3
v3 ^= 0x2000000000000000
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
v0 ^= 0x2000000000000000
v2 ^= 0xFF
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
v0, v1, v2, v3 = siphash_round(v0, v1, v2, v3)
return v0 ^ v1 ^ v2 ^ v3

View File

@@ -0,0 +1,841 @@
#!/usr/bin/env python3
# Copyright (c) 2014-2016 The Bitcoin Core developers
# Copyright (c) 2014-2017 The Dash Core developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
#
# Helpful routines for regression testing
#
import os
import sys
from binascii import hexlify, unhexlify
from base64 import b64encode
from decimal import Decimal, ROUND_DOWN
import json
import http.client
import random
import shutil
import subprocess
import tempfile
import time
import re
import errno
import logging
from . import coverage
from .authproxy import AuthServiceProxy, JSONRPCException
COVERAGE_DIR = None
logger = logging.getLogger("TestFramework.utils")
# The maximum number of nodes a single test can spawn
MAX_NODES = 15
# Don't assign rpc or p2p ports lower than this
PORT_MIN = 11000
# The number of ports to "reserve" for p2p and rpc, each
PORT_RANGE = 5000
BITCOIND_PROC_WAIT_TIMEOUT = 60
class PortSeed:
# Must be initialized with a unique integer for each process
n = None
#Set Mocktime default to OFF.
#MOCKTIME is only needed for scripts that use the
#cached version of the blockchain. If the cached
#version of the blockchain is used without MOCKTIME
#then the mempools will not sync due to IBD.
MOCKTIME = 0
def enable_mocktime():
#For backwared compatibility of the python scripts
#with previous versions of the cache, set MOCKTIME
#to Jan 1, 2014 + (201 * 10 * 60)
global MOCKTIME
MOCKTIME = 1414776313 + (201 * 10 * 60)
def set_mocktime(t):
global MOCKTIME
MOCKTIME = t
def disable_mocktime():
global MOCKTIME
MOCKTIME = 0
def get_mocktime():
return MOCKTIME
def enable_coverage(dirname):
"""Maintain a log of which RPC calls are made during testing."""
global COVERAGE_DIR
COVERAGE_DIR = dirname
def get_rpc_proxy(url, node_number, timeout=None):
"""
Args:
url (str): URL of the RPC server to call
node_number (int): the node number (or id) that this calls to
Kwargs:
timeout (int): HTTP timeout in seconds
Returns:
AuthServiceProxy. convenience object for making RPC calls.
"""
proxy_kwargs = {}
if timeout is not None:
proxy_kwargs['timeout'] = timeout
proxy = AuthServiceProxy(url, **proxy_kwargs)
proxy.url = url # store URL on proxy for info
coverage_logfile = coverage.get_filename(
COVERAGE_DIR, node_number) if COVERAGE_DIR else None
return coverage.AuthServiceProxyWrapper(proxy, coverage_logfile)
def get_evoznsync_status(node):
result = node.evoznsync("status")
return result['IsSynced']
def wait_to_sync(node, fast_znsync=False):
tm = 0
synced = False
while tm < 30:
synced = get_evoznsync_status(node)
if synced:
return
time.sleep(0.2)
if fast_znsync:
# skip mnsync states
node.evoznsync("next")
tm += 0.2
assert(synced)
def p2p_port(n):
assert(n <= MAX_NODES)
return PORT_MIN + n + (MAX_NODES * PortSeed.n) % (PORT_RANGE - 1 - MAX_NODES)
def rpc_port(n):
return PORT_MIN + PORT_RANGE + n + (MAX_NODES * PortSeed.n) % (PORT_RANGE - 1 - MAX_NODES)
def check_json_precision():
"""Make sure json library being used does not lose precision converting BTC values"""
n = Decimal("20000000.00000003")
satoshis = int(json.loads(json.dumps(float(n)))*1.0e8)
if satoshis != 2000000000000003:
raise RuntimeError("JSON encode/decode loses precision")
def count_bytes(hex_string):
return len(bytearray.fromhex(hex_string))
def bytes_to_hex_str(byte_str):
return hexlify(byte_str).decode('ascii')
def hex_str_to_bytes(hex_str):
return unhexlify(hex_str.encode('ascii'))
def str_to_b64str(string):
return b64encode(string.encode('utf-8')).decode('ascii')
def sync_blocks(rpc_connections, *, wait=1, timeout=60):
"""
Wait until everybody has the same tip.
sync_blocks needs to be called with an rpc_connections set that has least
one node already synced to the latest, stable tip, otherwise there's a
chance it might return before all nodes are stably synced.
"""
# Use getblockcount() instead of waitforblockheight() to determine the
# initial max height because the two RPCs look at different internal global
# variables (chainActive vs latestBlock) and the former gets updated
# earlier.
maxheight = max(x.getblockcount() for x in rpc_connections)
start_time = cur_time = time.time()
while cur_time <= start_time + timeout:
tips = [r.waitforblockheight(maxheight, int(wait * 1000)) for r in rpc_connections]
if all(t["height"] == maxheight for t in tips):
if all(t["hash"] == tips[0]["hash"] for t in tips):
return
raise AssertionError("Block sync failed, mismatched block hashes:{}".format(
"".join("\n {!r}".format(tip) for tip in tips)))
time.sleep(wait)
cur_time = time.time()
raise AssertionError("Block sync to height {} timed out:{}".format(
maxheight, "".join("\n {!r}".format(tip) for tip in tips)))
def sync_znodes(rpc_connections, *, timeout=60):
"""
Waits until every node has their znsync status is synced.
"""
start_time = cur_time = time.time()
while cur_time <= start_time + timeout:
statuses = [r.znsync("status") for r in rpc_connections]
if all(stat["IsSynced"] == True for stat in statuses):
return
cur_time = time.time()
raise AssertionError("Znode sync failed.")
def sync_chain(rpc_connections, *, wait=1, timeout=60):
"""
Wait until everybody has the same best block
"""
while timeout > 0:
best_hash = [x.getbestblockhash() for x in rpc_connections]
if best_hash == [best_hash[0]]*len(best_hash):
return
time.sleep(wait)
timeout -= wait
raise AssertionError("Chain sync failed: Best block hashes don't match")
def sync_mempools(rpc_connections, *, wait=1, timeout=60):
"""
Wait until everybody has the same transactions in their memory
pools
"""
while timeout > 0:
pool = set(rpc_connections[0].getrawmempool())
num_match = 1
for i in range(1, len(rpc_connections)):
if set(rpc_connections[i].getrawmempool()) == pool:
num_match = num_match+1
if num_match == len(rpc_connections):
return
time.sleep(wait)
timeout -= wait
raise AssertionError("Mempool sync failed")
def sync_znodes(rpc_connections, fast_mnsync=False):
for node in rpc_connections:
wait_to_sync(node, fast_mnsync)
bitcoind_processes = {}
def initialize_datadir(dirname, n):
datadir = os.path.join(dirname, "node"+str(n))
if not os.path.isdir(datadir):
os.makedirs(datadir)
rpc_u, rpc_p = rpc_auth_pair(n)
with open(os.path.join(datadir, "firo.conf"), 'w', encoding='utf8') as f:
f.write("regtest=1\n")
f.write("rpcuser=" + rpc_u + "\n")
f.write("rpcpassword=" + rpc_p + "\n")
f.write("port="+str(p2p_port(n))+"\n")
f.write("rpcport="+str(rpc_port(n))+"\n")
f.write("listenonion=0\n")
return datadir
def rpc_auth_pair(n):
return 'rpcuser💻' + str(n), 'rpcpass🔑' + str(n)
def rpc_url(i, rpchost=None):
rpc_u, rpc_p = rpc_auth_pair(i)
host = '127.0.0.1'
port = rpc_port(i)
if rpchost:
parts = rpchost.split(':')
if len(parts) == 2:
host, port = parts
else:
host = rpchost
return "http://%s:%s@%s:%d" % (rpc_u, rpc_p, host, int(port))
def wait_for_bitcoind_start(process, url, i):
'''
Wait for firod to start. This means that RPC is accessible and fully initialized.
Raise an exception if firod exits during initialization.
'''
while True:
if process.poll() is not None:
raise Exception('firod exited with status %i during initialization' % process.returncode)
try:
rpc = get_rpc_proxy(url, i)
blocks = rpc.getblockcount()
break # break out of loop on success
except IOError as e:
if e.errno != errno.ECONNREFUSED: # Port not yet open?
raise # unknown IO error
except JSONRPCException as e: # Initialization phase
if e.error['code'] != -28: # RPC in warmup?
raise # unknown JSON RPC exception
time.sleep(0.25)
def initialize_chain(test_dir, num_nodes, cachedir):
"""
Create a cache of a 200-block-long chain (with wallet) for MAX_NODES
Afterward, create num_nodes copies from the cache
"""
assert num_nodes <= MAX_NODES
create_cache = False
for i in range(MAX_NODES):
if not os.path.isdir(os.path.join(cachedir, 'node'+str(i))):
create_cache = True
break
if create_cache:
#find and delete old cache directories if any exist
for i in range(MAX_NODES):
if os.path.isdir(os.path.join(cachedir,"node"+str(i))):
shutil.rmtree(os.path.join(cachedir,"node"+str(i)))
# Create cache directories, run bitcoinds:
for i in range(MAX_NODES):
datadir=initialize_datadir(cachedir, i)
args = [ os.getenv("FIROD", "firod"), "-server", "-keypool=1", "-datadir="+datadir, "-discover=0" ]
if i > 0:
args.append("-connect=127.0.0.1:"+str(p2p_port(0)))
bitcoind_processes[i] = subprocess.Popen(args)
if os.getenv("PYTHON_DEBUG", ""):
print("initialize_chain: bitcoind started, waiting for RPC to come up")
wait_for_bitcoind_start(bitcoind_processes[i], rpc_url(i), i)
if os.getenv("PYTHON_DEBUG", ""):
print("initialize_chain: RPC successfully started")
rpcs = []
for i in range(MAX_NODES):
try:
rpcs.append(get_rpc_proxy(rpc_url(i), i))
except:
sys.stderr.write("Error connecting to "+url+"\n")
sys.exit(1)
# Create a 200-block-long chain; each of the 4 first nodes
# gets 25 mature blocks and 25 immature.
# Note: To preserve compatibility with older versions of
# initialize_chain, only 4 nodes will generate coins.
#
# blocks are created with timestamps 10 minutes apart
# starting from 2010 minutes in the past
enable_mocktime()
block_time = get_mocktime() - (201 * 10 * 60)
for i in range(2):
for peer in range(4):
for j in range(25):
set_node_times(rpcs, block_time)
rpcs[peer].generate(1)
block_time += 10*60
# Must sync before next peer starts generating blocks
sync_blocks(rpcs)
# Shut them down, and clean up cache directories:
stop_nodes(rpcs)
disable_mocktime()
for i in range(MAX_NODES):
try:
os.remove(log_filename(cachedir, i, "debug.log"))
os.remove(log_filename(cachedir, i, "db.log"))
os.remove(log_filename(cachedir, i, "peers.dat"))
os.remove(log_filename(cachedir, i, "fee_estimates.dat"))
except OSError:
pass
for i in range(num_nodes):
from_dir = os.path.join(cachedir, "node"+str(i))
to_dir = os.path.join(test_dir, "node"+str(i))
if from_dir != to_dir:
shutil.copytree(from_dir, to_dir)
initialize_datadir(test_dir, i) # Overwrite port/rpcport in bitcoin.conf
def initialize_chain_clean(test_dir, num_nodes):
"""
Create an empty blockchain and num_nodes wallets.
Useful if a test case wants complete control over initialization.
"""
for i in range(num_nodes):
datadir=initialize_datadir(test_dir, i)
def _rpchost_to_args(rpchost):
'''Convert optional IP:port spec to rpcconnect/rpcport args'''
if rpchost is None:
return []
match = re.match('(\[[0-9a-fA-f:]+\]|[^:]+)(?::([0-9]+))?$', rpchost)
if not match:
raise ValueError('Invalid RPC host spec ' + rpchost)
rpcconnect = match.group(1)
rpcport = match.group(2)
if rpcconnect.startswith('['): # remove IPv6 [...] wrapping
rpcconnect = rpcconnect[1:-1]
rv = ['-rpcconnect=' + rpcconnect]
if rpcport:
rv += ['-rpcport=' + rpcport]
return rv
def start_node(i, dirname, extra_args=None, rpchost=None, timewait=None, binary=None, redirect_stderr=False, stderr=None):
"""
Start a bitcoind and return RPC connection to it
"""
datadir = os.path.join(dirname, "node"+str(i))
if binary is None:
binary = os.getenv("FIROD", "firod")
args = [ binary, "-datadir="+datadir, "-server", "-keypool=1", "-discover=0", "-rest", "-dandelion=0", "-usemnemonic=0", "-mocktime="+str(get_mocktime()) ]
#Useful args for debugging
# "screen", "--",
# "gdb", "-x", "/tmp/gdb_run", "--args",
# Don't try auto backups (they fail a lot when running tests)
args += [ "-createwalletbackups=0" ]
if extra_args is not None: args.extend(extra_args)
# Allow to redirect stderr to stdout in case we expect some non-critical warnings/errors printed to stderr
# Otherwise the whole test would be considered to be failed in such cases
if redirect_stderr:
stderr = sys.stdout
bitcoind_processes[i] = subprocess.Popen(args, stderr=stderr)
logger.debug("start_node: firod started, waiting for RPC to come up")
url = rpc_url(i, rpchost)
wait_for_bitcoind_start(bitcoind_processes[i], url, i)
logger.debug("start_node: RPC successfully started")
proxy = get_rpc_proxy(url, i, timeout=timewait)
if COVERAGE_DIR:
coverage.write_all_rpc_commands(COVERAGE_DIR, proxy)
return proxy
def start_nodes(num_nodes, dirname, extra_args=None, rpchost=None, timewait=None, binary=None):
"""
Start multiple bitcoinds, return RPC connections to them
"""
if extra_args is None: extra_args = [ None for _ in range(num_nodes) ]
if binary is None: binary = [ None for _ in range(num_nodes) ]
rpcs = []
try:
for i in range(num_nodes):
rpcs.append(start_node(i, dirname, extra_args[i], rpchost, timewait=timewait, binary=binary[i]))
except: # If one node failed to start, stop the others
stop_nodes(rpcs)
raise
return rpcs
def copy_datadir(from_node, to_node, dirname):
from_datadir = os.path.join(dirname, "node"+str(from_node), "regtest")
to_datadir = os.path.join(dirname, "node"+str(to_node), "regtest")
dirs = ["blocks", "chainstate", "evodb", "llmq"]
for d in dirs:
try:
src = os.path.join(from_datadir, d)
dst = os.path.join(to_datadir, d)
shutil.copytree(src, dst)
except:
pass
def log_filename(dirname, n_node, logname):
return os.path.join(dirname, "node"+str(n_node), "regtest", logname)
def wait_node(i):
return_code = bitcoind_processes[i].wait(timeout=BITCOIND_PROC_WAIT_TIMEOUT)
assert_equal(return_code, 0)
del bitcoind_processes[i]
def stop_node(node, i, wait=True):
logger.debug("Stopping node %d" % i)
try:
node.stop()
except http.client.CannotSendRequest as e:
logger.exception("Unable to stop node")
if wait:
wait_node(i)
def stop_nodes(nodes, fast=True):
for i, node in enumerate(nodes):
stop_node(node, i, not fast)
if fast:
for i, node in enumerate(nodes):
wait_node(i)
assert not bitcoind_processes.values() # All connections must be gone now
def set_node_times(nodes, t):
for node in nodes:
node.setmocktime(t)
def connect_nodes(from_connection, node_num):
# NOTE: In next line p2p_port(0) was replaced by rpc_port(0).
ip_port = "127.0.0.1:"+str(p2p_port(node_num))
from_connection.addnode(ip_port, "onetry")
# poll until version handshake complete to avoid race conditions
# with transaction relaying
while any(peer['version'] == 0 for peer in from_connection.getpeerinfo()):
time.sleep(0.1)
def connect_nodes_bi(nodes, a, b):
connect_nodes(nodes[a], b)
connect_nodes(nodes[b], a)
def isolate_node(node, timeout=5):
node.setnetworkactive(False)
st = time.time()
while time.time() < st + timeout:
if node.getconnectioncount() == 0:
return
time.sleep(0.5)
raise AssertionError("disconnect_node timed out")
def reconnect_isolated_node(node, node_num):
node.setnetworkactive(True)
connect_nodes(node, node_num)
def find_output(node, txid, amount):
"""
Return index to output of txid with value amount
Raises exception if there is none.
"""
txdata = node.getrawtransaction(txid, 1)
for i in range(len(txdata["vout"])):
if txdata["vout"][i]["value"] == amount:
return i
raise RuntimeError("find_output txid %s : %s not found"%(txid,str(amount)))
def gather_inputs(from_node, amount_needed, confirmations_required=1):
"""
Return a random set of unspent txouts that are enough to pay amount_needed
"""
assert(confirmations_required >=0)
utxo = from_node.listunspent(confirmations_required)
random.shuffle(utxo)
inputs = []
total_in = Decimal("0.00000000")
while total_in < amount_needed and len(utxo) > 0:
t = utxo.pop()
total_in += t["amount"]
inputs.append({ "txid" : t["txid"], "vout" : t["vout"], "address" : t["address"] } )
if total_in < amount_needed:
raise RuntimeError("Insufficient funds: need %d, have %d"%(amount_needed, total_in))
return (total_in, inputs)
def make_change(from_node, amount_in, amount_out, fee):
"""
Create change output(s), return them
"""
outputs = {}
amount = amount_out+fee
change = amount_in - amount
if change > amount*2:
# Create an extra change output to break up big inputs
change_address = from_node.getnewaddress()
# Split change in two, being careful of rounding:
outputs[change_address] = Decimal(change/2).quantize(Decimal('0.00000001'), rounding=ROUND_DOWN)
change = amount_in - amount - outputs[change_address]
if change > 0:
outputs[from_node.getnewaddress()] = change
return outputs
def send_zeropri_transaction(from_node, to_node, amount, fee):
"""
Create&broadcast a zero-priority transaction.
Returns (txid, hex-encoded-txdata)
Ensures transaction is zero-priority by first creating a send-to-self,
then using its output
"""
# Create a send-to-self with confirmed inputs:
self_address = from_node.getnewaddress()
(total_in, inputs) = gather_inputs(from_node, amount+fee*2)
outputs = make_change(from_node, total_in, amount+fee, fee)
outputs[self_address] = float(amount+fee)
self_rawtx = from_node.createrawtransaction(inputs, outputs)
self_signresult = from_node.signrawtransaction(self_rawtx)
self_txid = from_node.sendrawtransaction(self_signresult["hex"], True)
vout = find_output(from_node, self_txid, amount+fee)
# Now immediately spend the output to create a 1-input, 1-output
# zero-priority transaction:
inputs = [ { "txid" : self_txid, "vout" : vout } ]
outputs = { to_node.getnewaddress() : float(amount) }
rawtx = from_node.createrawtransaction(inputs, outputs)
signresult = from_node.signrawtransaction(rawtx)
txid = from_node.sendrawtransaction(signresult["hex"], True)
return (txid, signresult["hex"])
def random_zeropri_transaction(nodes, amount, min_fee, fee_increment, fee_variants):
"""
Create a random zero-priority transaction.
Returns (txid, hex-encoded-transaction-data, fee)
"""
from_node = random.choice(nodes)
to_node = random.choice(nodes)
fee = min_fee + fee_increment*random.randint(0,fee_variants)
(txid, txhex) = send_zeropri_transaction(from_node, to_node, amount, fee)
return (txid, txhex, fee)
def random_transaction(nodes, amount, min_fee, fee_increment, fee_variants):
"""
Create a random transaction.
Returns (txid, hex-encoded-transaction-data, fee)
"""
from_node = random.choice(nodes)
to_node = random.choice(nodes)
fee = min_fee + fee_increment*random.randint(0,fee_variants)
(total_in, inputs) = gather_inputs(from_node, amount+fee)
outputs = make_change(from_node, total_in, amount, fee)
outputs[to_node.getnewaddress()] = float(amount)
rawtx = from_node.createrawtransaction(inputs, outputs)
signresult = from_node.signrawtransaction(rawtx)
txid = from_node.sendrawtransaction(signresult["hex"], True)
return (txid, signresult["hex"], fee)
def assert_fee_amount(fee, tx_size, fee_per_kB):
"""Assert the fee was in range"""
target_fee = tx_size * fee_per_kB / 1000
if fee < target_fee:
raise AssertionError("Fee of %s BTC too low! (Should be %s BTC)"%(str(fee), str(target_fee)))
# allow the wallet's estimation to be at most 2 bytes off
if fee > (tx_size + 2) * fee_per_kB / 1000:
raise AssertionError("Fee of %s BTC too high! (Should be %s BTC)"%(str(fee), str(target_fee)))
def assert_equal(thing1, thing2, *args):
if thing1 != thing2 or any(thing1 != arg for arg in args):
raise AssertionError("not(%s)" % " == ".join(str(arg) for arg in (thing1, thing2) + args))
def assert_greater_than(thing1, thing2):
if thing1 <= thing2:
raise AssertionError("%s <= %s"%(str(thing1),str(thing2)))
def assert_greater_than_or_equal(thing1, thing2):
if thing1 < thing2:
raise AssertionError("%s < %s"%(str(thing1),str(thing2)))
def assert_raises(exc, fun, *args, **kwds):
assert_raises_message(exc, None, fun, *args, **kwds)
def assert_raises_message(exc, message, fun, *args, **kwds):
try:
fun(*args, **kwds)
except exc as e:
if message is not None and message not in e.error['message']:
raise AssertionError("Expected substring not found:"+e.error['message'])
except Exception as e:
raise AssertionError("Unexpected exception raised: "+type(e).__name__)
else:
raise AssertionError("No exception raised")
def assert_raises_jsonrpc(code, message, fun, *args, **kwds):
"""Run an RPC and verify that a specific JSONRPC exception code and message is raised.
Calls function `fun` with arguments `args` and `kwds`. Catches a JSONRPCException
and verifies that the error code and message are as expected. Throws AssertionError if
no JSONRPCException was returned or if the error code/message are not as expected.
Args:
code (int), optional: the error code returned by the RPC call (defined
in src/rpc/protocol.h). Set to None if checking the error code is not required.
message (string), optional: [a substring of] the error string returned by the
RPC call. Set to None if checking the error string is not required
fun (function): the function to call. This should be the name of an RPC.
args*: positional arguments for the function.
kwds**: named arguments for the function.
"""
try:
fun(*args, **kwds)
except JSONRPCException as e:
# JSONRPCException was thrown as expected. Check the code and message values are correct.
if (code is not None) and (code != e.error["code"]):
raise AssertionError("Unexpected JSONRPC error code %i" % e.error["code"])
if (message is not None) and (message not in e.error['message']):
raise AssertionError("Expected substring not found:"+e.error['message'])
except Exception as e:
raise AssertionError("Unexpected exception raised: "+type(e).__name__)
else:
raise AssertionError("No exception raised")
def assert_is_hex_string(string):
try:
int(string, 16)
except Exception as e:
raise AssertionError(
"Couldn't interpret %r as hexadecimal; raised: %s" % (string, e))
def assert_is_hash_string(string, length=64):
if not isinstance(string, str):
raise AssertionError("Expected a string, got type %r" % type(string))
elif length and len(string) != length:
raise AssertionError(
"String of length %d expected; got %d" % (length, len(string)))
elif not re.match('[abcdef0-9]+$', string):
raise AssertionError(
"String %r contains invalid characters for a hash." % string)
def assert_array_result(object_array, to_match, expected, should_not_find = False):
"""
Pass in array of JSON objects, a dictionary with key/value pairs
to match against, and another dictionary with expected key/value
pairs.
If the should_not_find flag is true, to_match should not be found
in object_array
"""
if should_not_find == True:
assert_equal(expected, { })
num_matched = 0
for item in object_array:
all_match = True
for key,value in to_match.items():
if item[key] != value:
all_match = False
if not all_match:
continue
elif should_not_find == True:
num_matched = num_matched+1
for key,value in expected.items():
if item[key] != value:
raise AssertionError("%s : expected %s=%s"%(str(item), str(key), str(value)))
num_matched = num_matched+1
if num_matched == 0 and should_not_find != True:
raise AssertionError("No objects matched %s"%(str(to_match)))
if num_matched > 0 and should_not_find == True:
raise AssertionError("Objects were found %s"%(str(to_match)))
def satoshi_round(amount):
return Decimal(amount).quantize(Decimal('0.00000001'), rounding=ROUND_DOWN)
# Helper to create at least "count" utxos
# Pass in a fee that is sufficient for relay and mining new transactions.
def create_confirmed_utxos(fee, node, count):
node.generate(int(0.5*count)+101)
utxos = node.listunspent()
iterations = count - len(utxos)
addr1 = node.getnewaddress()
addr2 = node.getnewaddress()
if iterations <= 0:
return utxos
for i in range(iterations):
t = utxos.pop()
inputs = []
inputs.append({ "txid" : t["txid"], "vout" : t["vout"]})
outputs = {}
send_value = t['amount'] - fee
outputs[addr1] = satoshi_round(send_value/2)
outputs[addr2] = satoshi_round(send_value/2)
raw_tx = node.createrawtransaction(inputs, outputs)
signed_tx = node.signrawtransaction(raw_tx)["hex"]
txid = node.sendrawtransaction(signed_tx)
while (node.getmempoolinfo()['size'] > 0):
node.generate(1)
utxos = node.listunspent()
assert(len(utxos) >= count)
return utxos
# Create large OP_RETURN txouts that can be appended to a transaction
# to make it large (helper for constructing large transactions).
def gen_return_txouts():
# Some pre-processing to create a bunch of OP_RETURN txouts to insert into transactions we create
# So we have big transactions (and therefore can't fit very many into each block)
# create one script_pubkey
script_pubkey = "6a4d0200" #OP_RETURN OP_PUSH2 512 bytes
for i in range (512):
script_pubkey = script_pubkey + "01"
# concatenate 128 txouts of above script_pubkey which we'll insert before the txout for change
txouts = "81"
for k in range(128):
# add txout value
txouts = txouts + "0000000000000000"
# add length of script_pubkey
txouts = txouts + "fd0402"
# add script_pubkey
txouts = txouts + script_pubkey
return txouts
def create_tx(node, coinbase, to_address, amount):
inputs = [{ "txid" : coinbase, "vout" : 0}]
outputs = { to_address : amount }
rawtx = node.createrawtransaction(inputs, outputs)
signresult = node.signrawtransaction(rawtx)
assert_equal(signresult["complete"], True)
return signresult["hex"]
def create_tx_multi_input(node, inputs, outputs):
rawtx = node.createrawtransaction(inputs, outputs)
signresult = node.signrawtransaction(rawtx)
assert_equal(signresult["complete"], True)
return signresult["hex"]
# Create a spend of each passed-in utxo, splicing in "txouts" to each raw
# transaction to make it large. See gen_return_txouts() above.
def create_lots_of_big_transactions(node, txouts, utxos, num, fee):
addr = node.getnewaddress()
txids = []
for _ in range(num):
t = utxos.pop()
inputs=[{ "txid" : t["txid"], "vout" : t["vout"]}]
outputs = {}
change = t['amount'] - fee
outputs[addr] = satoshi_round(change)
rawtx = node.createrawtransaction(inputs, outputs)
newtx = rawtx[0:92]
newtx = newtx + txouts
newtx = newtx + rawtx[94:]
signresult = node.signrawtransaction(newtx, None, None, "NONE")
txid = node.sendrawtransaction(signresult["hex"], True)
txids.append(txid)
return txids
def mine_large_block(node, utxos=None):
# generate a 66k transaction,
# and 14 of them is close to the 1MB block limit
num = 14
txouts = gen_return_txouts()
utxos = utxos if utxos is not None else []
if len(utxos) < num:
utxos.clear()
utxos.extend(node.listunspent())
fee = 100 * node.getnetworkinfo()["relayfee"]
create_lots_of_big_transactions(node, txouts, utxos, num, fee=fee)
node.generate(1)
def get_bip9_status(node, key):
info = node.getblockchaininfo()
return info['bip9_softforks'][key]
def dumpprivkey_otac(node, address):
import re
error_text = ''
try:
return node.dumpprivkey(address)
except JSONRPCException as e:
error_text = e.error
else:
raise
otac_match = re.search("Your one time authorization code is: ([a-zA-Z0-9]+)", error_text['message'])
if not otac_match:
raise JSONRPCException(error_text)
return node.dumpprivkey(address, otac_match.groups()[0])
def get_znsync_status(node):
result = node.znsync("status")
return result['IsSynced']
def wait_to_sync_znodes(node, fast_znsync=False):
while True:
synced = get_znsync_status(node)
if synced:
break
time.sleep(0.2)
if fast_znsync:
# skip mnsync states
node.znsync("next")
def get_full_balance(node):
wallet_info = node.getwalletinfo()
return wallet_info["balance"] + wallet_info["immature_balance"] + wallet_info["unconfirmed_balance"]

View File

@@ -68,9 +68,6 @@ mininode_lock = RLock()
def sha256(s):
return hashlib.new('sha256', s).digest()
def ripemd160(s):
return hashlib.new('ripemd160', s).digest()
def hash256(s):
return sha256(sha256(s))
@@ -475,10 +472,11 @@ class CTransaction(object):
if flags != 0:
self.wit.vtxinwit = [CTxInWitness() for i in range(len(self.vin))]
self.wit.deserialize(f)
self.nLockTime = struct.unpack("<I", f.read(4))[0]
self.sha256 = None
self.hash = None
if self.nVersion > 2:
if self.nVersion >= 2:
self.strDZeel = deser_string(f)
def serialize_without_witness(self):

View File

@@ -37,10 +37,6 @@ MAX_SCRIPT_OPCODES = 201
OPCODE_NAMES = {}
def hash160(s):
return hashlib.new('ripemd160', sha256(s)).digest()
_opcode_instances = []
class CScriptOp(int):
"""A single script opcode"""

View File

@@ -51,9 +51,6 @@ MSG_TYPE_MASK = 0xffffffff >> 2
def sha256(s):
return hashlib.new('sha256', s).digest()
def ripemd160(s):
return hashlib.new('ripemd160', s).digest()
def hash256(s):
return sha256(sha256(s))

View File

@@ -32,7 +32,7 @@ class DASHInterface(BTCInterface):
words = self.seedToMnemonic(key)
mnemonic_passphrase = ''
self.rpc_callback('upgradetohd', [words, mnemonic_passphrase, self._wallet_passphrase])
self.rpc_wallet('upgradetohd', [words, mnemonic_passphrase, self._wallet_passphrase])
self._have_checked_seed = False
if self._wallet_passphrase != '':
self.unlockWallet(self._wallet_passphrase)
@@ -42,7 +42,7 @@ class DASHInterface(BTCInterface):
def checkExpectedSeed(self, key_hash: str):
try:
rv = self.rpc_callback('dumphdinfo')
rv = self.rpc_wallet('dumphdinfo')
entropy = Mnemonic('english').to_entropy(rv['mnemonic'].split(' '))
entropy_hash = self.getAddressHashFromKey(entropy)[::-1].hex()
self._have_checked_seed = True
@@ -53,10 +53,10 @@ class DASHInterface(BTCInterface):
def withdrawCoin(self, value, addr_to, subfee):
params = [addr_to, value, '', '', subfee, False, False, self._conf_target]
return self.rpc_callback('sendtoaddress', params)
return self.rpc_wallet('sendtoaddress', params)
def getSpendableBalance(self) -> int:
return self.make_int(self.rpc_callback('getwalletinfo')['balance'])
return self.make_int(self.rpc_wallet('getwalletinfo')['balance'])
def getScriptForPubkeyHash(self, pkh: bytes) -> bytearray:
# Return P2PKH
@@ -72,7 +72,7 @@ class DASHInterface(BTCInterface):
def findTxnByHash(self, txid_hex: str):
# Only works for wallet txns
try:
rv = self.rpc_callback('gettransaction', [txid_hex])
rv = self.rpc_wallet('gettransaction', [txid_hex])
except Exception as ex:
self._log.debug('findTxnByHash getrawtransaction failed: {}'.format(txid_hex))
return None

View File

@@ -1,26 +1,33 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (c) 2022 tecnovert
# Copyright (c) 2022-2023 tecnovert
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import random
import hashlib
from .btc import BTCInterface, find_vout_for_address_from_txobj
from basicswap.chainparams import Coins
from .btc import BTCInterface, find_vout_for_address_from_txobj
from basicswap.util import (
i2b,
ensure,
)
from basicswap.rpc import make_rpc_func
from basicswap.util.crypto import hash160
from basicswap.util.address import decodeAddress
from basicswap.contrib.test_framework.script import (
from basicswap.chainparams import Coins
from basicswap.interface.contrib.firo_test_framework.script import (
CScript,
OP_0,
OP_DUP,
OP_EQUAL,
OP_HASH160,
OP_CHECKSIG,
OP_EQUALVERIFY,
hash160,
)
from basicswap.contrib.test_framework.messages import (
from basicswap.interface.contrib.firo_test_framework.mininode import (
CBlock,
FromHex,
CTransaction,
)
@@ -30,6 +37,14 @@ 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)
# No multiwallet support
self.rpc_wallet = make_rpc_func(self._rpcport, self._rpcauth, host=self._rpc_host)
def checkWallets(self) -> int:
return 1
def getExchangeName(self, exchange_name):
return 'zcoin'
@@ -38,9 +53,9 @@ class FIROInterface(BTCInterface):
pass
def getNewAddress(self, use_segwit, label='swap_receive'):
return self.rpc_callback('getnewaddress', [label])
# addr_plain = self.rpc_callback('getnewaddress', [label])
# return self.rpc_callback('addwitnessaddress', [addr_plain])
return self.rpc('getnewaddress', [label])
# addr_plain = self.rpc('getnewaddress', [label])
# return self.rpc('addwitnessaddress', [addr_plain])
def decodeAddress(self, address):
return decodeAddress(address)[1:]
@@ -52,11 +67,11 @@ class FIROInterface(BTCInterface):
raise ValueError('TODO')
def isWatchOnlyAddress(self, address):
addr_info = self.rpc_callback('validateaddress', [address])
addr_info = self.rpc('validateaddress', [address])
return addr_info['iswatchonly']
def isAddressMine(self, address: str, or_watch_only: bool = False) -> bool:
addr_info = self.rpc_callback('validateaddress', [address])
addr_info = self.rpc('validateaddress', [address])
if not or_watch_only:
return addr_info['ismine']
return addr_info['ismine'] or addr_info['iswatchonly']
@@ -67,24 +82,23 @@ class FIROInterface(BTCInterface):
if not self.isAddressMine(address, or_watch_only=True):
# Expects P2WSH nested in BIP16_P2SH
ro = self.rpc_callback('importaddress', [lock_tx_dest.hex(), 'bid lock', False, True])
addr_info = self.rpc_callback('validateaddress', [address])
ro = self.rpc('importaddress', [lock_tx_dest.hex(), 'bid lock', False, True])
addr_info = self.rpc('validateaddress', [address])
return address
def getLockTxHeightFiro(self, txid, lock_script, bid_amount, rescan_from, find_index=False):
def getLockTxHeight(self, txid, dest_address, bid_amount, rescan_from, find_index: bool = False):
# Add watchonly address and rescan if required
lock_tx_dest = self.getScriptDest(lock_script)
dest_address = self.encodeScriptDest(lock_tx_dest)
if not self.isAddressMine(dest_address, or_watch_only=True):
self.rpc_callback('importaddress', [lock_tx_dest.hex(), 'bid lock', False, True])
self.importWatchOnlyAddress(dest_address, 'bid')
self._log.info('Imported watch-only addr: {}'.format(dest_address))
self._log.info('Rescanning {} chain from height: {}'.format(self.coin_name(), rescan_from))
self.rpc_callback('rescanblockchain', [rescan_from])
self.rescanBlockchainForAddress(rescan_from, dest_address)
return_txid = True if txid is None else False
if txid is None:
txns = self.rpc_callback('listunspent', [0, 9999999, [dest_address, ]])
txns = self.rpc('listunspent', [0, 9999999, [dest_address, ]])
for tx in txns:
if self.make_int(tx['amount']) == bid_amount:
@@ -95,11 +109,11 @@ class FIROInterface(BTCInterface):
return None
try:
tx = self.rpc_callback('gettransaction', [txid.hex()])
tx = self.rpc('gettransaction', [txid.hex()])
block_height = 0
if 'blockhash' in tx:
block_header = self.rpc_callback('getblockheader', [tx['blockhash']])
block_header = self.rpc('getblockheader', [tx['blockhash']])
block_height = block_header['height']
rv = {
@@ -111,7 +125,7 @@ class FIROInterface(BTCInterface):
return None
if find_index:
tx_obj = self.rpc_callback('decoderawtransaction', [tx['hex']])
tx_obj = self.rpc('decoderawtransaction', [tx['hex']])
rv['index'] = find_vout_for_address_from_txobj(tx_obj, dest_address)
if return_txid:
@@ -130,11 +144,11 @@ class FIROInterface(BTCInterface):
return self.fundTx(tx_bytes, feerate)
def signTxWithWallet(self, tx):
rv = self.rpc_callback('signrawtransaction', [tx.hex()])
rv = self.rpc('signrawtransaction', [tx.hex()])
return bytes.fromhex(rv['hex'])
def createRawFundedTransaction(self, addr_to: str, amount: int, sub_fee: bool = False, lock_unspents: bool = True) -> str:
txn = self.rpc_callback('createrawtransaction', [[], {addr_to: self.format_amount(amount)}])
txn = self.rpc('createrawtransaction', [[], {addr_to: self.format_amount(amount)}])
fee_rate, fee_src = self.get_fee_rate(self._conf_target)
self._log.debug(f'Fee rate: {fee_rate}, source: {fee_src}, block target: {self._conf_target}')
options = {
@@ -143,25 +157,23 @@ class FIROInterface(BTCInterface):
}
if sub_fee:
options['subtractFeeFromOutputs'] = [0,]
return self.rpc_callback('fundrawtransaction', [txn, options])['hex']
return self.rpc('fundrawtransaction', [txn, options])['hex']
def createRawSignedTransaction(self, addr_to, amount) -> str:
txn_funded = self.createRawFundedTransaction(addr_to, amount)
return self.rpc_callback('signrawtransaction', [txn_funded])['hex']
return self.rpc('signrawtransaction', [txn_funded])['hex']
def getScriptForPubkeyHash(self, pkh: bytes) -> bytearray:
# Return P2PKH
return CScript([OP_DUP, OP_HASH160, pkh, OP_EQUALVERIFY, OP_CHECKSIG])
def getScriptDest(self, script: bytearray) -> bytearray:
# P2WSH nested in BIP16_P2SH
# P2SH
script_hash = hashlib.sha256(script).digest()
assert len(script_hash) == 32
script_hash_hash = hash160(script_hash)
assert len(script_hash_hash) == 20
script_hash = hash160(script)
assert len(script_hash) == 20
return CScript([OP_HASH160, script_hash_hash, OP_EQUAL])
return CScript([OP_HASH160, script_hash, OP_EQUAL])
def getSeedHash(self, seed: bytes) -> bytes:
return hash160(seed)[::-1]
@@ -171,18 +183,19 @@ class FIROInterface(BTCInterface):
script_hash = script_dest[2:-1]
return self.sh_to_address(script_hash)
def getScriptScriptSig(self, script: bytes) -> bytearray:
return CScript([OP_0, hashlib.sha256(script).digest()])
def getDestForScriptHash(self, script_hash):
assert len(script_hash) == 20
return CScript([OP_HASH160, script_hash, OP_EQUAL])
def withdrawCoin(self, value, addr_to, subfee):
params = [addr_to, value, '', '', subfee]
return self.rpc_callback('sendtoaddress', params)
return self.rpc('sendtoaddress', params)
def getWalletSeedID(self):
return self.rpc_callback('getwalletinfo')['hdmasterkeyid']
return self.rpc('getwalletinfo')['hdmasterkeyid']
def getSpendableBalance(self) -> int:
return self.make_int(self.rpc_callback('getwalletinfo')['balance'])
return self.make_int(self.rpc('getwalletinfo')['balance'])
def getBLockSpendTxFee(self, tx, fee_rate: int) -> int:
add_bytes = 107
@@ -193,13 +206,13 @@ class FIROInterface(BTCInterface):
def signTxWithKey(self, tx: bytes, key: bytes) -> bytes:
key_wif = self.encodeKey(key)
rv = self.rpc_callback('signrawtransaction', [tx.hex(), [], [key_wif, ]])
rv = self.rpc('signrawtransaction', [tx.hex(), [], [key_wif, ]])
return bytes.fromhex(rv['hex'])
def findTxnByHash(self, txid_hex: str):
# Only works for wallet txns
try:
rv = self.rpc_callback('gettransaction', [txid_hex])
rv = self.rpc('gettransaction', [txid_hex])
except Exception as ex:
self._log.debug('findTxnByHash getrawtransaction failed: {}'.format(txid_hex))
return None
@@ -207,3 +220,148 @@ class FIROInterface(BTCInterface):
block_height = self.getBlockHeader(rv['blockhash'])['height']
return {'txid': txid_hex, 'amount': 0, 'height': block_height}
return None
def getProofOfFunds(self, amount_for, extra_commit_bytes):
# TODO: Lock unspent and use same output/s to fund bid
unspents_by_addr = dict()
unspents = self.rpc('listunspent')
for u in unspents:
if u['spendable'] is not True:
continue
if u['address'] not in unspents_by_addr:
unspents_by_addr[u['address']] = {'total': 0, 'utxos': []}
utxo_amount: int = self.make_int(u['amount'], r=1)
unspents_by_addr[u['address']]['total'] += utxo_amount
unspents_by_addr[u['address']]['utxos'].append((utxo_amount, u['txid'], u['vout']))
max_utxos: int = 4
viable_addrs = []
for addr, data in unspents_by_addr.items():
if data['total'] >= amount_for:
# Sort from largest to smallest amount
sorted_utxos = sorted(data['utxos'], key=lambda x: x[0])
# Max outputs required to reach amount_for
utxos_req: int = 0
sum_value: int = 0
for utxo in sorted_utxos:
sum_value += utxo[0]
utxos_req += 1
if sum_value >= amount_for:
break
if utxos_req <= max_utxos:
viable_addrs.append(addr)
continue
ensure(len(viable_addrs) > 0, 'Could not find address with enough funds for proof')
sign_for_addr: str = random.choice(viable_addrs)
self._log.debug('sign_for_addr %s', sign_for_addr)
prove_utxos = []
sorted_utxos = sorted(unspents_by_addr[sign_for_addr]['utxos'], key=lambda x: x[0])
hasher = hashlib.sha256()
sum_value: int = 0
for utxo in sorted_utxos:
sum_value += utxo[0]
outpoint = (bytes.fromhex(utxo[1]), utxo[2])
prove_utxos.append(outpoint)
hasher.update(outpoint[0])
hasher.update(outpoint[1].to_bytes(2, 'big'))
if sum_value >= amount_for:
break
utxos_hash = hasher.digest()
self._log.debug('sign_for_addr %s', sign_for_addr)
if self.using_segwit(): # TODO: Use isSegwitAddress when scantxoutset can use combo
# 'Address does not refer to key' for non p2pkh
pkh = self.decodeAddress(sign_for_addr)
sign_for_addr = self.pkh_to_address(pkh)
self._log.debug('sign_for_addr converted %s', sign_for_addr)
signature = self.rpc('signmessage', [sign_for_addr, sign_for_addr + '_swap_proof_' + utxos_hash.hex() + extra_commit_bytes.hex()])
return (sign_for_addr, signature, prove_utxos)
def verifyProofOfFunds(self, address, signature, utxos, extra_commit_bytes):
hasher = hashlib.sha256()
sum_value: int = 0
for outpoint in utxos:
hasher.update(outpoint[0])
hasher.update(outpoint[1].to_bytes(2, 'big'))
utxos_hash = hasher.digest()
passed = self.verifyMessage(address, address + '_swap_proof_' + utxos_hash.hex() + extra_commit_bytes.hex(), signature)
ensure(passed is True, 'Proof of funds signature invalid')
if self.using_segwit():
address = self.encodeSegwitAddress(decodeAddress(address)[1:])
sum_value: int = 0
for outpoint in utxos:
txout = self.rpc('gettxout', [outpoint[0].hex(), outpoint[1]])
sum_value += self.make_int(txout['value'])
return sum_value
def rescanBlockchainForAddress(self, height_start: int, addr_find: str):
# Very ugly workaround for missing `rescanblockchain` rpc command
chain_blocks: int = self.getChainHeight()
current_height: int = chain_blocks
block_hash = self.rpc('getblockhash', [current_height])
script_hash: bytes = self.decodeAddress(addr_find)
find_scriptPubKey = self.getDestForScriptHash(script_hash)
while current_height > height_start:
block_hash = self.rpc('getblockhash', [current_height])
block = self.rpc('getblock', [block_hash, False])
decoded_block = CBlock()
decoded_block = FromHex(decoded_block, block)
for tx in decoded_block.vtx:
for txo in tx.vout:
if txo.scriptPubKey == find_scriptPubKey:
tx.rehash()
txid = i2b(tx.sha256)
self._log.info('Found output to addr: {} in tx {} in block {}'.format(addr_find, txid.hex(), block_hash))
self._log.info('rescanblockchain hack invalidateblock {}'.format(block_hash))
self.rpc('invalidateblock', [block_hash])
self.rpc('reconsiderblock', [block_hash])
return
current_height -= 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)
tx_rv = []
for tx in decoded_block.vtx:
tx_hex = tx.serialize_with_witness().hex()
tx_dec = self.rpc('decoderawtransaction', [tx_hex])
if 'hex' not in tx_dec:
tx_dec['hex'] = tx_hex
tx_rv.append(tx_dec)
block_rv = {
'hash': block_hash,
'tx': tx_rv,
'confirmations': block_header['confirmations'],
'height': block_header['height'],
'version': block_header['version'],
'merkleroot': block_header['merkleroot'],
}
return block_rv

View File

@@ -1,15 +1,122 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (c) 2020 tecnovert
# Copyright (c) 2020-2023 tecnovert
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
from .btc import BTCInterface
from basicswap.chainparams import Coins
from basicswap.rpc import make_rpc_func
from basicswap.chainparams import Coins, chainparams
class LTCInterface(BTCInterface):
@staticmethod
def coin_type():
return Coins.LTC
def __init__(self, coin_settings, network, swap_client=None):
super(LTCInterface, self).__init__(coin_settings, network, swap_client)
self._rpc_wallet_mweb = 'mweb'
self.rpc_wallet_mweb = make_rpc_func(self._rpcport, self._rpcauth, host=self._rpc_host, wallet=self._rpc_wallet_mweb)
def getNewMwebAddress(self, use_segwit=False, label='swap_receive') -> str:
return self.rpc_wallet_mweb('getnewaddress', [label, 'mweb'])
def getNewStealthAddress(self, label=''):
return self.getNewMwebAddress(False, label)
def withdrawCoin(self, value, type_from: str, addr_to: str, subfee: bool) -> str:
params = [addr_to, value, '', '', subfee, True, self._conf_target]
if type_from == 'mweb':
return self.rpc_wallet_mweb('sendtoaddress', params)
return self.rpc_wallet('sendtoaddress', params)
def getWalletInfo(self):
rv = super(LTCInterface, self).getWalletInfo()
mweb_info = self.rpc_wallet_mweb('getwalletinfo')
rv['mweb_balance'] = mweb_info['balance']
rv['mweb_unconfirmed'] = mweb_info['unconfirmed_balance']
rv['mweb_immature'] = mweb_info['immature_balance']
# Add unconfirmed mweb -> plain txns to the unconfirmed_balance
txns = self.rpc_wallet('listtransactions')
for tx in reversed(txns):
amount: float = tx.get('amount', 0.0)
if tx['confirmations'] == 0 and tx.get('mweb_out', None) and amount > 0.0:
rv['unconfirmed_balance'] += amount
return rv
class LTCInterfaceMWEB(LTCInterface):
@staticmethod
def coin_type():
return Coins.LTC_MWEB
def __init__(self, coin_settings, network, swap_client=None):
super(LTCInterfaceMWEB, self).__init__(coin_settings, network, swap_client)
self._rpc_wallet = 'mweb'
self.rpc_wallet = make_rpc_func(self._rpcport, self._rpcauth, host=self._rpc_host, wallet=self._rpc_wallet)
def chainparams(self):
return chainparams[Coins.LTC]
def chainparams_network(self):
return chainparams[Coins.LTC][self._network]
def coin_name(self) -> str:
coin_chainparams = chainparams[Coins.LTC]
if coin_chainparams.get('use_ticker_as_name', False):
return coin_chainparams['ticker'] + ' MWEB'
return coin_chainparams['name'].capitalize() + ' MWEB'
def ticker(self) -> str:
ticker = chainparams[Coins.LTC]['ticker']
if self._network == 'testnet':
ticker = 't' + ticker
elif self._network == 'regtest':
ticker = 'rt' + ticker
return ticker + '_MWEB'
def getNewAddress(self, use_segwit=False, label='swap_receive') -> str:
return self.getNewMwebAddress()
def has_mweb_wallet(self) -> bool:
return 'mweb' in self.rpc('listwallets')
def init_wallet(self, password=None):
# If system is encrypted mweb wallet will be created at first unlock
self._log.info('init_wallet - {}'.format(self.ticker()))
self._log.info('Creating mweb wallet for {}.'.format(self.coin_name()))
# wallet_name, disable_private_keys, blank, passphrase, avoid_reuse, descriptors, load_on_startup
self.rpc('createwallet', ['mweb', False, True, password, False, False, True])
if password is not None:
# Max timeout value, ~3 years
self.rpc_wallet('walletpassphrase', [password, 100000000])
if self.getWalletSeedID() == 'Not found':
self._sc.initialiseWallet(self.coin_type())
# Workaround to trigger mweb_spk_man->LoadMWEBKeychain()
self.rpc('unloadwallet', ['mweb'])
self.rpc('loadwallet', ['mweb'])
if password is not None:
self.rpc_wallet('walletpassphrase', [password, 100000000])
self.rpc_wallet('keypoolrefill')
def unlockWallet(self, password: str):
if password == '':
return
self._log.info('unlockWallet - {}'.format(self.ticker()))
if not self.has_mweb_wallet():
self.init_wallet(password)
else:
# Max timeout value, ~3 years
self.rpc_wallet('walletpassphrase', [password, 100000000])
self._sc.checkWalletSeed(self.coin_type())

View File

@@ -5,12 +5,16 @@
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import random
import hashlib
from io import BytesIO
from coincurve.keys import (
PublicKey,
PrivateKey,
)
from .btc import BTCInterface, find_vout_for_address_from_txobj, findOutput
from basicswap.rpc import make_rpc_func
from basicswap.chainparams import Coins
from basicswap.interface.contrib.nav_test_framework.mininode import (
CTxIn,
@@ -22,6 +26,7 @@ from basicswap.interface.contrib.nav_test_framework.mininode import (
FromHex,
uint256_from_str,
)
from basicswap.util.crypto import hash160
from basicswap.util.address import (
decodeWif,
pubkeyToAddress,
@@ -36,7 +41,6 @@ from basicswap.basicswap_util import (
)
from basicswap.interface.contrib.nav_test_framework.script import (
hash160,
CScript,
OP_0,
OP_EQUAL,
@@ -60,6 +64,14 @@ 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)
# No multiwallet support
self.rpc_wallet = make_rpc_func(self._rpcport, self._rpcauth, host=self._rpc_host)
def checkWallets(self) -> int:
return 1
def use_p2shp2wsh(self) -> bool:
# p2sh-p2wsh
return True
@@ -72,24 +84,24 @@ class NAVInterface(BTCInterface):
pass
def getWalletSeedID(self):
return self.rpc_callback('getwalletinfo')['hdmasterkeyid']
return self.rpc('getwalletinfo')['hdmasterkeyid']
def withdrawCoin(self, value, addr_to: str, subfee: bool):
strdzeel = ''
params = [addr_to, value, '', '', strdzeel, subfee]
return self.rpc_callback('sendtoaddress', params)
return self.rpc('sendtoaddress', params)
def getSpendableBalance(self) -> int:
return self.make_int(self.rpc_callback('getwalletinfo')['balance'])
return self.make_int(self.rpc('getwalletinfo')['balance'])
def signTxWithWallet(self, tx: bytes) -> bytes:
rv = self.rpc_callback('signrawtransaction', [tx.hex()])
rv = self.rpc('signrawtransaction', [tx.hex()])
return bytes.fromhex(rv['hex'])
def checkExpectedSeed(self, key_hash: str):
try:
rv = self.rpc_callback('dumpmnemonic')
rv = self.rpc('dumpmnemonic')
entropy = Mnemonic('english').to_entropy(rv.split(' '))
entropy_hash = self.getAddressHashFromKey(entropy)[::-1].hex()
@@ -148,33 +160,120 @@ class NAVInterface(BTCInterface):
tx.wit.vtxinwit[0].scriptWitness.stack = stack
return tx.serialize_with_witness()
def verifyProofOfFunds(self, address, signature, extra_commit_bytes):
self._log.warning('verifyProofOfFunds TODO')
# TODO: Port scantxoutset or external lookup or read utxodb directly
return 999999 * self.COIN()
def getProofOfFunds(self, amount_for, extra_commit_bytes):
# TODO: Lock unspent and use same output/s to fund bid
unspents_by_addr = dict()
unspents = self.rpc('listunspent')
for u in unspents:
if u['spendable'] is not True:
continue
if u['address'] not in unspents_by_addr:
unspents_by_addr[u['address']] = {'total': 0, 'utxos': []}
utxo_amount: int = self.make_int(u['amount'], r=1)
unspents_by_addr[u['address']]['total'] += utxo_amount
unspents_by_addr[u['address']]['utxos'].append((utxo_amount, u['txid'], u['vout']))
max_utxos: int = 4
viable_addrs = []
for addr, data in unspents_by_addr.items():
if data['total'] >= amount_for:
# Sort from largest to smallest amount
sorted_utxos = sorted(data['utxos'], key=lambda x: x[0])
# Max outputs required to reach amount_for
utxos_req: int = 0
sum_value: int = 0
for utxo in sorted_utxos:
sum_value += utxo[0]
utxos_req += 1
if sum_value >= amount_for:
break
if utxos_req <= max_utxos:
viable_addrs.append(addr)
continue
ensure(len(viable_addrs) > 0, 'Could not find address with enough funds for proof')
sign_for_addr: str = random.choice(viable_addrs)
self._log.debug('sign_for_addr %s', sign_for_addr)
prove_utxos = []
sorted_utxos = sorted(unspents_by_addr[sign_for_addr]['utxos'], key=lambda x: x[0])
hasher = hashlib.sha256()
sum_value: int = 0
for utxo in sorted_utxos:
sum_value += utxo[0]
outpoint = (bytes.fromhex(utxo[1]), utxo[2])
prove_utxos.append(outpoint)
hasher.update(outpoint[0])
hasher.update(outpoint[1].to_bytes(2, 'big'))
if sum_value >= amount_for:
break
utxos_hash = hasher.digest()
self._log.debug('sign_for_addr %s', sign_for_addr)
if self.using_segwit(): # TODO: Use isSegwitAddress when scantxoutset can use combo
# 'Address does not refer to key' for non p2pkh
addr_info = self.rpc('validateaddress', [addr, ])
if 'isscript' in addr_info and addr_info['isscript'] and 'hex' in addr_info:
pkh = bytes.fromhex(addr_info['hex'])[2:]
sign_for_addr = self.pkh_to_address(pkh)
self._log.debug('sign_for_addr converted %s', sign_for_addr)
signature = self.rpc('signmessage', [sign_for_addr, sign_for_addr + '_swap_proof_' + utxos_hash.hex() + extra_commit_bytes.hex()])
return (sign_for_addr, signature, prove_utxos)
def verifyProofOfFunds(self, address, signature, utxos, extra_commit_bytes):
hasher = hashlib.sha256()
sum_value: int = 0
for outpoint in utxos:
hasher.update(outpoint[0])
hasher.update(outpoint[1].to_bytes(2, 'big'))
utxos_hash = hasher.digest()
passed = self.verifyMessage(address, address + '_swap_proof_' + utxos_hash.hex() + extra_commit_bytes.hex(), signature)
ensure(passed is True, 'Proof of funds signature invalid')
if self.using_segwit():
address = self.encodeSegwitAddress(self.decodeAddress(address)[1:])
sum_value: int = 0
for outpoint in utxos:
txout = self.rpc('gettxout', [outpoint[0].hex(), outpoint[1]])
sum_value += self.make_int(txout['value'])
return sum_value
def createRawFundedTransaction(self, addr_to: str, amount: int, sub_fee: bool = False, lock_unspents: bool = True) -> str:
txn = self.rpc_callback('createrawtransaction', [[], {addr_to: self.format_amount(amount)}])
txn = self.rpc('createrawtransaction', [[], {addr_to: self.format_amount(amount)}])
fee_rate, fee_src = self.get_fee_rate(self._conf_target)
self._log.debug(f'Fee rate: {fee_rate}, source: {fee_src}, block target: {self._conf_target}')
if sub_fee:
raise ValueError('Navcoin fundrawtransaction is missing the subtractFeeFromOutputs parameter')
# options['subtractFeeFromOutputs'] = [0,]
return self.fundTx(txn, fee_rate, lock_unspents)
fee_rate = self.make_int(fee_rate, r=1)
return self.fundTx(txn, fee_rate, lock_unspents).hex()
def isAddressMine(self, address: str, or_watch_only: bool = False) -> bool:
addr_info = self.rpc_callback('validateaddress', [address])
addr_info = self.rpc('validateaddress', [address])
if not or_watch_only:
return addr_info['ismine']
return addr_info['ismine'] or addr_info['iswatchonly']
def createRawSignedTransaction(self, addr_to, amount) -> str:
txn_funded = self.createRawFundedTransaction(addr_to, amount)
return self.rpc_callback('signrawtransaction', [txn_funded])['hex']
return self.rpc('signrawtransaction', [txn_funded])['hex']
def getBlockchainInfo(self):
rv = self.rpc_callback('getblockchaininfo')
rv = self.rpc('getblockchaininfo')
synced = round(rv['verificationprogress'], 3)
if synced >= 0.997:
rv['verificationprogress'] = 1.0
@@ -188,7 +287,7 @@ class NAVInterface(BTCInterface):
return pubkeyToAddress(self.chainparams_network()['script_address'], script)
def find_prevout_info(self, txn_hex: str, txn_script: bytes):
txjs = self.rpc_callback('decoderawtransaction', [txn_hex])
txjs = self.rpc('decoderawtransaction', [txn_hex])
n = getVoutByScriptPubKey(txjs, self.getScriptDest(txn_script).hex())
return {
@@ -199,36 +298,10 @@ class NAVInterface(BTCInterface):
'amount': txjs['vout'][n]['value']
}
def getProofOfFunds(self, amount_for, extra_commit_bytes):
# TODO: Lock unspent and use same output/s to fund bid
unspent_addr = self.getUnspentsByAddr()
sign_for_addr = None
for addr, value in unspent_addr.items():
if value >= amount_for:
sign_for_addr = addr
break
ensure(sign_for_addr is not None, 'Could not find address with enough funds for proof')
self._log.debug('sign_for_addr %s', sign_for_addr)
if self.using_segwit(): # TODO: Use isSegwitAddress when scantxoutset can use combo
# 'Address does not refer to key' for non p2pkh
addr_info = self.rpc_callback('validateaddress', [addr, ])
if 'isscript' in addr_info and addr_info['isscript'] and 'hex' in addr_info:
pkh = bytes.fromhex(addr_info['hex'])[2:]
sign_for_addr = self.pkh_to_address(pkh)
self._log.debug('sign_for_addr converted %s', sign_for_addr)
signature = self.rpc_callback('signmessage', [sign_for_addr, sign_for_addr + '_swap_proof_' + extra_commit_bytes.hex()])
return (sign_for_addr, signature)
def getNewAddress(self, use_segwit: bool, label: str = 'swap_receive') -> str:
address: str = self.rpc_callback('getnewaddress', [label,])
address: str = self.rpc('getnewaddress', [label,])
if use_segwit:
return self.rpc_callback('addwitnessaddress', [address,])
return self.rpc('addwitnessaddress', [address,])
return address
def createRedeemTxn(self, prevout, output_addr: str, output_value: int, txn_script: bytes) -> str:
@@ -321,15 +394,15 @@ class NAVInterface(BTCInterface):
chain_blocks: int = self.getChainHeight()
current_height: int = chain_blocks
block_hash = self.rpc_callback('getblockhash', [current_height])
block_hash = self.rpc('getblockhash', [current_height])
script_hash: bytes = self.decodeAddress(addr_find)
find_scriptPubKey = self.getDestForScriptHash(script_hash)
while current_height > height_start:
block_hash = self.rpc_callback('getblockhash', [current_height])
block_hash = self.rpc('getblockhash', [current_height])
block = self.rpc_callback('getblock', [block_hash, False])
block = self.rpc('getblock', [block_hash, False])
decoded_block = CBlock()
decoded_block = FromHex(decoded_block, block)
for tx in decoded_block.vtx:
@@ -339,8 +412,8 @@ class NAVInterface(BTCInterface):
txid = i2b(tx.sha256)
self._log.info('Found output to addr: {} in tx {} in block {}'.format(addr_find, txid.hex(), block_hash))
self._log.info('rescanblockchain hack invalidateblock {}'.format(block_hash))
self.rpc_callback('invalidateblock', [block_hash])
self.rpc_callback('reconsiderblock', [block_hash])
self.rpc('invalidateblock', [block_hash])
self.rpc('reconsiderblock', [block_hash])
return
current_height -= 1
@@ -350,13 +423,12 @@ class NAVInterface(BTCInterface):
if not self.isAddressMine(dest_address, or_watch_only=True):
self.importWatchOnlyAddress(dest_address, 'bid')
self._log.info('Imported watch-only addr: {}'.format(dest_address))
# Importing triggers a rescan
self._log.info('Rescanning {} chain from height: {}'.format(self.coin_name(), rescan_from))
self.rescanBlockchainForAddress(rescan_from, dest_address)
return_txid = True if txid is None else False
if txid is None:
txns = self.rpc_callback('listunspent', [0, 9999999, [dest_address, ]])
txns = self.rpc('listunspent', [0, 9999999, [dest_address, ]])
for tx in txns:
if self.make_int(tx['amount']) == bid_amount:
@@ -367,11 +439,11 @@ class NAVInterface(BTCInterface):
return None
try:
tx = self.rpc_callback('gettransaction', [txid.hex()])
tx = self.rpc('gettransaction', [txid.hex()])
block_height = 0
if 'blockhash' in tx:
block_header = self.rpc_callback('getblockheader', [tx['blockhash']])
block_header = self.rpc('getblockheader', [tx['blockhash']])
block_height = block_header['height']
rv = {
@@ -383,7 +455,7 @@ class NAVInterface(BTCInterface):
return None
if find_index:
tx_obj = self.rpc_callback('decoderawtransaction', [tx['hex']])
tx_obj = self.rpc('decoderawtransaction', [tx['hex']])
rv['index'] = find_vout_for_address_from_txobj(tx_obj, dest_address)
if return_txid:
@@ -393,15 +465,15 @@ class NAVInterface(BTCInterface):
def getBlockWithTxns(self, block_hash):
# TODO: Bypass decoderawtransaction and getblockheader
block = self.rpc_callback('getblock', [block_hash, False])
block_header = self.rpc_callback('getblockheader', [block_hash])
block = self.rpc('getblock', [block_hash, False])
block_header = self.rpc('getblockheader', [block_hash])
decoded_block = CBlock()
decoded_block = FromHex(decoded_block, block)
tx_rv = []
for tx in decoded_block.vtx:
tx_hex = tx.serialize_with_witness().hex()
tx_dec = self.rpc_callback('decoderawtransaction', [tx_hex])
tx_dec = self.rpc('decoderawtransaction', [tx_hex])
if 'hex' not in tx_dec:
tx_dec['hex'] = tx_hex
@@ -442,7 +514,7 @@ class NAVInterface(BTCInterface):
def spendBLockTx(self, chain_b_lock_txid: bytes, address_to: str, kbv: bytes, kbs: bytes, cb_swap_value: int, b_fee: int, restore_height: int) -> bytes:
self._log.info('spendBLockTx %s:\n', chain_b_lock_txid.hex())
wtx = self.rpc_callback('gettransaction', [chain_b_lock_txid.hex(), ])
wtx = self.rpc('gettransaction', [chain_b_lock_txid.hex(), ])
lock_tx = self.loadTx(bytes.fromhex(wtx['hex']))
Kbs = self.getPubkey(kbs)
@@ -487,7 +559,7 @@ class NAVInterface(BTCInterface):
def findTxnByHash(self, txid_hex: str):
# Only works for wallet txns
try:
rv = self.rpc_callback('gettransaction', [txid_hex])
rv = self.rpc('gettransaction', [txid_hex])
except Exception as ex:
self._log.debug('findTxnByHash getrawtransaction failed: {}'.format(txid_hex))
return None
@@ -503,17 +575,17 @@ class NAVInterface(BTCInterface):
return tx.serialize()
def fundTx(self, tx, feerate, lock_unspents: bool = True):
def fundTx(self, tx_hex: str, feerate: int, lock_unspents: bool = True):
feerate_str = self.format_amount(feerate)
# TODO: unlock unspents if bid cancelled
options = {
'lockUnspents': lock_unspents,
'feeRate': feerate_str,
}
rv = self.rpc_callback('fundrawtransaction', [tx.hex(), options])
rv = self.rpc('fundrawtransaction', [tx_hex, options])
# Sign transaction then strip witness data to fill scriptsig
rv = self.rpc_callback('signrawtransaction', [rv['hex']])
rv = self.rpc('signrawtransaction', [rv['hex']])
tx_signed = self.loadTx(bytes.fromhex(rv['hex']))
if len(tx_signed.vin) != len(tx_signed.wit.vtxinwit):
@@ -524,8 +596,8 @@ class NAVInterface(BTCInterface):
return tx_signed.serialize_without_witness()
def fundSCLockTx(self, tx_bytes: bytes, feerate, vkbv=None):
tx_funded = self.fundTx(tx_bytes, feerate)
def fundSCLockTx(self, tx_bytes: bytes, feerate, vkbv=None) -> bytes:
tx_funded = self.fundTx(tx_bytes.hex(), feerate)
return tx_funded
def createSCLockRefundTx(self, tx_lock_bytes, script_lock, Kal, Kaf, lock1_value, csv_val, tx_fee_rate, vkbv=None):

View File

@@ -19,7 +19,7 @@ class NMCInterface(BTCInterface):
def getLockTxHeight(self, txid, dest_address, bid_amount, rescan_from, find_index=False):
self._log.debug('[rm] scantxoutset start') # scantxoutset is slow
ro = self.rpc_callback('scantxoutset', ['start', ['addr({})'.format(dest_address)]]) # TODO: Use combo(address) where possible
ro = self.rpc('scantxoutset', ['start', ['addr({})'.format(dest_address)]]) # TODO: Use combo(address) where possible
self._log.debug('[rm] scantxoutset end')
return_txid = True if txid is None else False
for o in ro['unspents']:

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (c) 2020-2023 tecnovert
# Copyright (c) 2020-2024 tecnovert
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -83,14 +83,14 @@ class PARTInterface(BTCInterface):
return True
def getNewAddress(self, use_segwit, label='swap_receive') -> str:
return self.rpc_callback('getnewaddress', [label])
return self.rpc_wallet('getnewaddress', [label])
def getNewStealthAddress(self, label='swap_stealth') -> str:
return self.rpc_callback('getnewstealthaddress', [label])
return self.rpc_wallet('getnewstealthaddress', [label])
def haveSpentIndex(self):
version = self.getDaemonVersion()
index_info = self.rpc_callback('getinsightinfo' if int(str(version)[:2]) > 19 else 'getindexinfo')
index_info = self.rpc('getinsightinfo' if int(str(version)[:2]) > 19 else 'getindexinfo')
return index_info['spentindex']
def initialiseWallet(self, key):
@@ -98,14 +98,14 @@ class PARTInterface(BTCInterface):
def withdrawCoin(self, value, addr_to, subfee):
params = [addr_to, value, '', '', subfee, '', True, self._conf_target]
return self.rpc_callback('sendtoaddress', params)
return self.rpc_wallet('sendtoaddress', params)
def sendTypeTo(self, type_from, type_to, value, addr_to, subfee):
params = [type_from, type_to,
[{'address': addr_to, 'amount': value, 'subfee': subfee}, ],
'', '', self._anon_tx_ring_size, 1, False,
{'conf_target': self._conf_target}]
return self.rpc_callback('sendtypeto', params)
return self.rpc_wallet('sendtypeto', params)
def getScriptForPubkeyHash(self, pkh: bytes) -> CScript:
return CScript([OP_DUP, OP_HASH160, pkh, OP_EQUALVERIFY, OP_CHECKSIG])
@@ -122,7 +122,7 @@ class PARTInterface(BTCInterface):
return length
def getWalletRestoreHeight(self) -> int:
start_time = self.rpc_callback('getwalletinfo')['keypoololdest']
start_time = self.rpc_wallet('getwalletinfo')['keypoololdest']
blockchaininfo = self.getBlockchainInfo()
best_block = blockchaininfo['bestblockhash']
@@ -132,8 +132,8 @@ class PARTInterface(BTCInterface):
raise ValueError('{} chain isn\'t synced.'.format(self.coin_name()))
self._log.debug('Finding block at time: {}'.format(start_time))
block_hash = self.rpc_callback('getblockhashafter', [start_time])
block_header = self.rpc_callback('getblockheader', [block_hash])
block_hash = self.rpc('getblockhashafter', [start_time])
block_header = self.rpc('getblockheader', [block_hash])
return block_header['height']
def getHTLCSpendTxVSize(self, redeem: bool = True) -> int:
@@ -141,6 +141,17 @@ class PARTInterface(BTCInterface):
tx_vsize += 204 if redeem else 187
return tx_vsize
def getUnspentsByAddr(self):
unspent_addr = dict()
unspent = self.rpc_wallet('listunspent')
for u in unspent:
if u['spendable'] is not True:
continue
if 'address' not in u:
continue
unspent_addr[u['address']] = unspent_addr.get(u['address'], 0) + self.make_int(u['amount'], r=1)
return unspent_addr
class PARTInterfaceBlind(PARTInterface):
@staticmethod
@@ -171,16 +182,16 @@ class PARTInterfaceBlind(PARTInterface):
if txo['type'] != 'blind':
continue
try:
blinded_info = self.rpc_callback('rewindrangeproof', [txo['rangeproof'], txo['valueCommitment'], nonce.hex()])
blinded_info = self.rpc('rewindrangeproof', [txo['rangeproof'], txo['valueCommitment'], nonce.hex()])
output_n = txo['n']
self.rpc_callback('rewindrangeproof', [txo['rangeproof'], txo['valueCommitment'], nonce.hex()])
self.rpc('rewindrangeproof', [txo['rangeproof'], txo['valueCommitment'], nonce.hex()])
break
except Exception as e:
self._log.debug('Searching for locked output: {}'.format(str(e)))
continue
# Should not be possible for commitment not to match
v = self.rpc_callback('verifycommitment', [txo['valueCommitment'], blinded_info['blind'], blinded_info['amount']])
v = self.rpc('verifycommitment', [txo['valueCommitment'], blinded_info['blind'], blinded_info['amount']])
ensure(v['result'] is True, 'verifycommitment failed')
return output_n, blinded_info
@@ -195,7 +206,7 @@ class PARTInterfaceBlind(PARTInterface):
inputs = []
outputs = [{'type': 'blind', 'amount': self.format_amount(value), 'address': p2wsh_addr, 'nonce': nonce.hex(), 'data': ephemeral_pubkey.hex()}]
params = [inputs, outputs]
rv = self.rpc_callback('createrawparttransaction', params)
rv = self.rpc_wallet('createrawparttransaction', params)
tx_bytes = bytes.fromhex(rv['hex'])
return tx_bytes
@@ -207,11 +218,11 @@ class PARTInterfaceBlind(PARTInterface):
tx_hex = tx_bytes.hex()
nonce = self.getScriptLockTxNonce(vkbv)
tx_obj = self.rpc_callback('decoderawtransaction', [tx_hex])
tx_obj = self.rpc('decoderawtransaction', [tx_hex])
assert (len(tx_obj['vout']) == 1)
txo = tx_obj['vout'][0]
blinded_info = self.rpc_callback('rewindrangeproof', [txo['rangeproof'], txo['valueCommitment'], nonce.hex()])
blinded_info = self.rpc('rewindrangeproof', [txo['rangeproof'], txo['valueCommitment'], nonce.hex()])
outputs_info = {0: {'value': blinded_info['amount'], 'blind': blinded_info['blind'], 'nonce': nonce.hex()}}
@@ -219,11 +230,11 @@ class PARTInterfaceBlind(PARTInterface):
'lockUnspents': True,
'feeRate': feerate_str,
}
rv = self.rpc_callback('fundrawtransactionfrom', ['blind', tx_hex, {}, outputs_info, options])
rv = self.rpc('fundrawtransactionfrom', ['blind', tx_hex, {}, outputs_info, options])
return bytes.fromhex(rv['hex'])
def createSCLockRefundTx(self, tx_lock_bytes, script_lock, Kal, Kaf, lock1_value, csv_val, tx_fee_rate, vkbv):
lock_tx_obj = self.rpc_callback('decoderawtransaction', [tx_lock_bytes.hex()])
lock_tx_obj = self.rpc('decoderawtransaction', [tx_lock_bytes.hex()])
assert (self.getTxid(tx_lock_bytes).hex() == lock_tx_obj['txid'])
# Nonce is derived from vkbv, ephemeral_key isn't used
ephemeral_key = self.getNewSecretKey()
@@ -244,7 +255,7 @@ class PARTInterfaceBlind(PARTInterface):
inputs = [{'txid': tx_lock_id, 'vout': spend_n, 'sequence': lock1_value, 'blindingfactor': input_blinded_info['blind']}]
outputs = [{'type': 'blind', 'amount': locked_coin, 'address': p2wsh_addr, 'nonce': output_nonce.hex(), 'data': ephemeral_pubkey.hex()}]
params = [inputs, outputs]
rv = self.rpc_callback('createrawparttransaction', params)
rv = self.rpc_wallet('createrawparttransaction', params)
lock_refund_tx_hex = rv['hex']
# Set dummy witness data for fee estimation
@@ -261,7 +272,7 @@ class PARTInterfaceBlind(PARTInterface):
'feeRate': self.format_amount(tx_fee_rate),
'subtractFeeFromOutputs': [0, ]
}
rv = self.rpc_callback('fundrawtransactionfrom', ['blind', lock_refund_tx_hex, inputs_info, outputs_info, options])
rv = self.rpc_wallet('fundrawtransactionfrom', ['blind', lock_refund_tx_hex, inputs_info, outputs_info, options])
lock_refund_tx_hex = rv['hex']
for vout, txo in rv['output_amounts'].items():
@@ -275,7 +286,7 @@ class PARTInterfaceBlind(PARTInterface):
# The follower will sign the multisig path with a signature encumbered by the leader's coinB spend pubkey
# If the leader publishes the decrypted signature the leader's coinB spend privatekey will be revealed to the follower
lock_refund_tx_obj = self.rpc_callback('decoderawtransaction', [tx_lock_refund_bytes.hex()])
lock_refund_tx_obj = self.rpc('decoderawtransaction', [tx_lock_refund_bytes.hex()])
# Nonce is derived from vkbv
nonce = self.getScriptLockRefundTxNonce(vkbv)
@@ -285,7 +296,7 @@ class PARTInterfaceBlind(PARTInterface):
tx_lock_refund_id = lock_refund_tx_obj['txid']
addr_out = self.pkh_to_address(pkh_refund_to)
addr_info = self.rpc_callback('getaddressinfo', [addr_out])
addr_info = self.rpc_wallet('getaddressinfo', [addr_out])
output_pubkey_hex = addr_info['pubkey']
# Follower won't be able to decode output to check amount, shouldn't matter as fee is public and output is to leader, sum has to balance
@@ -293,7 +304,7 @@ class PARTInterfaceBlind(PARTInterface):
inputs = [{'txid': tx_lock_refund_id, 'vout': spend_n, 'sequence': 0, 'blindingfactor': input_blinded_info['blind']}]
outputs = [{'type': 'blind', 'amount': input_blinded_info['amount'], 'address': addr_out, 'pubkey': output_pubkey_hex}]
params = [inputs, outputs]
rv = self.rpc_callback('createrawparttransaction', params)
rv = self.rpc_wallet('createrawparttransaction', params)
lock_refund_spend_tx_hex = rv['hex']
# Set dummy witness data for fee estimation
@@ -311,7 +322,7 @@ class PARTInterfaceBlind(PARTInterface):
'subtractFeeFromOutputs': [0, ]
}
rv = self.rpc_callback('fundrawtransactionfrom', ['blind', lock_refund_spend_tx_hex, inputs_info, outputs_info, options])
rv = self.rpc_wallet('fundrawtransactionfrom', ['blind', lock_refund_spend_tx_hex, inputs_info, outputs_info, options])
lock_refund_spend_tx_hex = rv['hex']
return bytes.fromhex(lock_refund_spend_tx_hex)
@@ -321,7 +332,7 @@ class PARTInterfaceBlind(PARTInterface):
Kal, Kaf,
feerate,
check_lock_tx_inputs, vkbv):
lock_tx_obj = self.rpc_callback('decoderawtransaction', [tx_bytes.hex()])
lock_tx_obj = self.rpc('decoderawtransaction', [tx_bytes.hex()])
lock_txid_hex = lock_tx_obj['txid']
self._log.info('Verifying lock tx: {}.'.format(lock_txid_hex))
@@ -363,7 +374,7 @@ class PARTInterfaceBlind(PARTInterface):
def verifySCLockRefundTx(self, tx_bytes, lock_tx_bytes, script_out,
prevout_id, prevout_n, prevout_seq, prevout_script,
Kal, Kaf, csv_val_expect, swap_value, feerate, vkbv):
lock_refund_tx_obj = self.rpc_callback('decoderawtransaction', [tx_bytes.hex()])
lock_refund_tx_obj = self.rpc('decoderawtransaction', [tx_bytes.hex()])
lock_refund_txid_hex = lock_refund_tx_obj['txid']
self._log.info('Verifying lock refund tx: {}.'.format(lock_refund_txid_hex))
@@ -396,10 +407,10 @@ class PARTInterfaceBlind(PARTInterface):
ensure(C == Kaf, 'Bad script pubkey')
# Check rangeproofs and commitments sum
lock_tx_obj = self.rpc_callback('decoderawtransaction', [lock_tx_bytes.hex()])
lock_tx_obj = self.rpc('decoderawtransaction', [lock_tx_bytes.hex()])
prevout = lock_tx_obj['vout'][prevout_n]
prevtxns = [{'txid': prevout_id.hex(), 'vout': prevout_n, 'scriptPubKey': prevout['scriptPubKey']['hex'], 'amount_commitment': prevout['valueCommitment']}]
rv = self.rpc_callback('verifyrawtransaction', [tx_bytes.hex(), prevtxns])
rv = self.rpc('verifyrawtransaction', [tx_bytes.hex(), prevtxns])
ensure(rv['outputs_valid'] is True, 'Invalid outputs')
ensure(rv['inputs_valid'] is True, 'Invalid inputs')
@@ -422,7 +433,7 @@ class PARTInterfaceBlind(PARTInterface):
lock_refund_tx_id, prevout_script,
Kal,
prevout_n, prevout_value, feerate, vkbv):
lock_refund_spend_tx_obj = self.rpc_callback('decoderawtransaction', [tx_bytes.hex()])
lock_refund_spend_tx_obj = self.rpc('decoderawtransaction', [tx_bytes.hex()])
lock_refund_spend_txid_hex = lock_refund_spend_tx_obj['txid']
self._log.info('Verifying lock refund spend tx: {}.'.format(lock_refund_spend_txid_hex))
@@ -441,10 +452,10 @@ class PARTInterfaceBlind(PARTInterface):
# Follower is not concerned with them as they pay to leader
# Check rangeproofs and commitments sum
lock_refund_tx_obj = self.rpc_callback('decoderawtransaction', [lock_refund_tx_bytes.hex()])
lock_refund_tx_obj = self.rpc('decoderawtransaction', [lock_refund_tx_bytes.hex()])
prevout = lock_refund_tx_obj['vout'][prevout_n]
prevtxns = [{'txid': lock_refund_tx_id.hex(), 'vout': prevout_n, 'scriptPubKey': prevout['scriptPubKey']['hex'], 'amount_commitment': prevout['valueCommitment']}]
rv = self.rpc_callback('verifyrawtransaction', [tx_bytes.hex(), prevtxns])
rv = self.rpc('verifyrawtransaction', [tx_bytes.hex(), prevtxns])
ensure(rv['outputs_valid'] is True, 'Invalid outputs')
ensure(rv['inputs_valid'] is True, 'Invalid inputs')
@@ -459,28 +470,28 @@ class PARTInterfaceBlind(PARTInterface):
return True
def getLockTxSwapOutputValue(self, bid, xmr_swap):
lock_tx_obj = self.rpc_callback('decoderawtransaction', [xmr_swap.a_lock_tx.hex()])
lock_tx_obj = self.rpc('decoderawtransaction', [xmr_swap.a_lock_tx.hex()])
nonce = self.getScriptLockTxNonce(xmr_swap.vkbv)
output_n, _ = self.findOutputByNonce(lock_tx_obj, nonce)
ensure(output_n is not None, 'Output not found in tx')
return bytes.fromhex(lock_tx_obj['vout'][output_n]['valueCommitment'])
def getLockRefundTxSwapOutputValue(self, bid, xmr_swap):
lock_refund_tx_obj = self.rpc_callback('decoderawtransaction', [xmr_swap.a_lock_refund_tx.hex()])
lock_refund_tx_obj = self.rpc('decoderawtransaction', [xmr_swap.a_lock_refund_tx.hex()])
nonce = self.getScriptLockRefundTxNonce(xmr_swap.vkbv)
output_n, _ = self.findOutputByNonce(lock_refund_tx_obj, nonce)
ensure(output_n is not None, 'Output not found in tx')
return bytes.fromhex(lock_refund_tx_obj['vout'][output_n]['valueCommitment'])
def getLockRefundTxSwapOutput(self, xmr_swap):
lock_refund_tx_obj = self.rpc_callback('decoderawtransaction', [xmr_swap.a_lock_refund_tx.hex()])
lock_refund_tx_obj = self.rpc('decoderawtransaction', [xmr_swap.a_lock_refund_tx.hex()])
nonce = self.getScriptLockRefundTxNonce(xmr_swap.vkbv)
output_n, _ = self.findOutputByNonce(lock_refund_tx_obj, nonce)
ensure(output_n is not None, 'Output not found in tx')
return output_n
def createSCLockSpendTx(self, tx_lock_bytes: bytes, script_lock: bytes, pk_dest: bytes, tx_fee_rate: int, vkbv: bytes, fee_info={}) -> bytes:
lock_tx_obj = self.rpc_callback('decoderawtransaction', [tx_lock_bytes.hex()])
lock_tx_obj = self.rpc('decoderawtransaction', [tx_lock_bytes.hex()])
lock_txid_hex = lock_tx_obj['txid']
ensure(lock_tx_obj['version'] == self.txVersion(), 'Bad version')
@@ -496,7 +507,7 @@ class PARTInterfaceBlind(PARTInterface):
inputs = [{'txid': lock_txid_hex, 'vout': spend_n, 'sequence': 0, 'blindingfactor': blinded_info['blind']}]
outputs = [{'type': 'blind', 'amount': blinded_info['amount'], 'address': addr_out, 'pubkey': pk_dest.hex()}]
params = [inputs, outputs]
rv = self.rpc_callback('createrawparttransaction', params)
rv = self.rpc_wallet('createrawparttransaction', params)
lock_spend_tx_hex = rv['hex']
# Set dummy witness data for fee estimation
@@ -513,9 +524,9 @@ class PARTInterfaceBlind(PARTInterface):
'subtractFeeFromOutputs': [0, ]
}
rv = self.rpc_callback('fundrawtransactionfrom', ['blind', lock_spend_tx_hex, inputs_info, outputs_info, options])
rv = self.rpc_wallet('fundrawtransactionfrom', ['blind', lock_spend_tx_hex, inputs_info, outputs_info, options])
lock_spend_tx_hex = rv['hex']
lock_spend_tx_obj = self.rpc_callback('decoderawtransaction', [lock_spend_tx_hex])
lock_spend_tx_obj = self.rpc('decoderawtransaction', [lock_spend_tx_hex])
pay_fee = make_int(lock_spend_tx_obj['vout'][0]['ct_fee'])
# lock_spend_tx_hex does not include the dummy witness stack
@@ -535,7 +546,7 @@ class PARTInterfaceBlind(PARTInterface):
def verifySCLockSpendTx(self, tx_bytes,
lock_tx_bytes, lock_tx_script,
a_pk_f, feerate, vkbv):
lock_spend_tx_obj = self.rpc_callback('decoderawtransaction', [tx_bytes.hex()])
lock_spend_tx_obj = self.rpc('decoderawtransaction', [tx_bytes.hex()])
lock_spend_txid_hex = lock_spend_tx_obj['txid']
self._log.info('Verifying lock spend tx: {}.'.format(lock_spend_txid_hex))
@@ -543,7 +554,7 @@ class PARTInterfaceBlind(PARTInterface):
ensure(lock_spend_tx_obj['locktime'] == 0, 'Bad nLockTime')
ensure(len(lock_spend_tx_obj['vin']) == 1, 'tx doesn\'t have one input')
lock_tx_obj = self.rpc_callback('decoderawtransaction', [lock_tx_bytes.hex()])
lock_tx_obj = self.rpc('decoderawtransaction', [lock_tx_bytes.hex()])
lock_txid_hex = lock_tx_obj['txid']
# Find the output of the lock tx to verify
@@ -559,7 +570,7 @@ class PARTInterfaceBlind(PARTInterface):
ensure(len(lock_spend_tx_obj['vout']) == 3, 'tx doesn\'t have three outputs')
addr_out = self.pubkey_to_address(a_pk_f)
privkey = self.rpc_callback('dumpprivkey', [addr_out])
privkey = self.rpc_wallet('dumpprivkey', [addr_out])
# Find output:
output_blinded_info = None
@@ -568,7 +579,7 @@ class PARTInterfaceBlind(PARTInterface):
if txo['type'] != 'blind':
continue
try:
output_blinded_info = self.rpc_callback('rewindrangeproof', [txo['rangeproof'], txo['valueCommitment'], privkey, txo['data_hex']])
output_blinded_info = self.rpc('rewindrangeproof', [txo['rangeproof'], txo['valueCommitment'], privkey, txo['data_hex']])
output_n = txo['n']
break
except Exception as e:
@@ -577,13 +588,13 @@ class PARTInterfaceBlind(PARTInterface):
ensure(output_n is not None, 'Output not found in tx')
# Commitment
v = self.rpc_callback('verifycommitment', [lock_spend_tx_obj['vout'][output_n]['valueCommitment'], output_blinded_info['blind'], output_blinded_info['amount']])
v = self.rpc('verifycommitment', [lock_spend_tx_obj['vout'][output_n]['valueCommitment'], output_blinded_info['blind'], output_blinded_info['amount']])
ensure(v['result'] is True, 'verifycommitment failed')
# Check rangeproofs and commitments sum
prevout = lock_tx_obj['vout'][spend_n]
prevtxns = [{'txid': lock_txid_hex, 'vout': spend_n, 'scriptPubKey': prevout['scriptPubKey']['hex'], 'amount_commitment': prevout['valueCommitment']}]
rv = self.rpc_callback('verifyrawtransaction', [tx_bytes.hex(), prevtxns])
rv = self.rpc('verifyrawtransaction', [tx_bytes.hex(), prevtxns])
ensure(rv['outputs_valid'] is True, 'Invalid outputs')
ensure(rv['inputs_valid'] is True, 'Invalid inputs')
@@ -607,7 +618,7 @@ class PARTInterfaceBlind(PARTInterface):
def createSCLockRefundSpendToFTx(self, tx_lock_refund_bytes, script_lock_refund, pkh_dest, tx_fee_rate, vkbv):
# lock refund swipe tx
# Sends the coinA locked coin to the follower
lock_refund_tx_obj = self.rpc_callback('decoderawtransaction', [tx_lock_refund_bytes.hex()])
lock_refund_tx_obj = self.rpc('decoderawtransaction', [tx_lock_refund_bytes.hex()])
nonce = self.getScriptLockRefundTxNonce(vkbv)
# Find the output of the lock refund tx to spend
@@ -616,7 +627,7 @@ class PARTInterfaceBlind(PARTInterface):
tx_lock_refund_id = lock_refund_tx_obj['txid']
addr_out = self.pkh_to_address(pkh_dest)
addr_info = self.rpc_callback('getaddressinfo', [addr_out])
addr_info = self.rpc_wallet('getaddressinfo', [addr_out])
output_pubkey_hex = addr_info['pubkey']
A, B, lock2_value, C = self.extractScriptLockRefundScriptValues(script_lock_refund)
@@ -626,7 +637,7 @@ class PARTInterfaceBlind(PARTInterface):
inputs = [{'txid': tx_lock_refund_id, 'vout': spend_n, 'sequence': lock2_value, 'blindingfactor': input_blinded_info['blind']}]
outputs = [{'type': 'blind', 'amount': input_blinded_info['amount'], 'address': addr_out, 'pubkey': output_pubkey_hex}]
params = [inputs, outputs]
rv = self.rpc_callback('createrawparttransaction', params)
rv = self.rpc_wallet('createrawparttransaction', params)
lock_refund_swipe_tx_hex = rv['hex']
@@ -645,13 +656,13 @@ class PARTInterfaceBlind(PARTInterface):
'subtractFeeFromOutputs': [0, ]
}
rv = self.rpc_callback('fundrawtransactionfrom', ['blind', lock_refund_swipe_tx_hex, inputs_info, outputs_info, options])
rv = self.rpc_wallet('fundrawtransactionfrom', ['blind', lock_refund_swipe_tx_hex, inputs_info, outputs_info, options])
lock_refund_swipe_tx_hex = rv['hex']
return bytes.fromhex(lock_refund_swipe_tx_hex)
def getSpendableBalance(self) -> int:
return self.make_int(self.rpc_callback('getbalances')['mine']['blind_trusted'])
return self.make_int(self.rpc_wallet('getbalances')['mine']['blind_trusted'])
def publishBLockTx(self, vkbv: bytes, Kbs: bytes, output_amount: int, feerate: int, delay_for: int = 10, unlock_time: int = 0) -> bytes:
Kbv = self.getPubkey(vkbv)
@@ -664,7 +675,7 @@ class PARTInterfaceBlind(PARTInterface):
'', '', self._anon_tx_ring_size, 1, False,
{'conf_target': self._conf_target, 'blind_watchonly_visible': True}]
txid = self.rpc_callback('sendtypeto', params)
txid = self.rpc_wallet('sendtypeto', params)
return bytes.fromhex(txid)
def findTxB(self, kbv, Kbs, cb_swap_value, cb_block_confirmed, restore_height: int, bid_sender: bool):
@@ -675,17 +686,17 @@ class PARTInterfaceBlind(PARTInterface):
if bid_sender:
cb_swap_value *= -1
else:
addr_info = self.rpc_callback('getaddressinfo', [sx_addr])
addr_info = self.rpc_wallet('getaddressinfo', [sx_addr])
if not addr_info['iswatchonly']:
wif_prefix = self.chainparams_network()['key_prefix']
wif_scan_key = toWIF(wif_prefix, kbv)
self.rpc_callback('importstealthaddress', [wif_scan_key, Kbs.hex()])
self.rpc_wallet('importstealthaddress', [wif_scan_key, Kbs.hex()])
self._log.info('Imported watch-only sx_addr: {}'.format(sx_addr))
self._log.info('Rescanning {} chain from height: {}'.format(self.coin_name(), restore_height))
self.rpc_callback('rescanblockchain', [restore_height])
self.rpc_wallet('rescanblockchain', [restore_height])
params = [{'include_watchonly': True, 'search': sx_addr}]
txns = self.rpc_callback('filtertransactions', params)
txns = self.rpc_wallet('filtertransactions', params)
if len(txns) == 1:
tx = txns[0]
@@ -695,7 +706,7 @@ class PARTInterfaceBlind(PARTInterface):
if make_int(tx['outputs'][0]['amount']) == cb_swap_value:
height = 0
if tx['confirmations'] > 0:
chain_height = self.rpc_callback('getblockcount')
chain_height = self.rpc('getblockcount')
height = chain_height - (tx['confirmations'] - 1)
return {'txid': tx['txid'], 'amount': cb_swap_value, 'height': height}
else:
@@ -707,20 +718,20 @@ class PARTInterfaceBlind(PARTInterface):
Kbv = self.getPubkey(kbv)
Kbs = self.getPubkey(kbs)
sx_addr = self.formatStealthAddress(Kbv, Kbs)
addr_info = self.rpc_callback('getaddressinfo', [sx_addr])
addr_info = self.rpc_wallet('getaddressinfo', [sx_addr])
if not addr_info['ismine']:
wif_prefix = self.chainparams_network()['key_prefix']
wif_scan_key = toWIF(wif_prefix, kbv)
wif_spend_key = toWIF(wif_prefix, kbs)
self.rpc_callback('importstealthaddress', [wif_scan_key, wif_spend_key])
self.rpc_wallet('importstealthaddress', [wif_scan_key, wif_spend_key])
self._log.info('Imported spend key for sx_addr: {}'.format(sx_addr))
self._log.info('Rescanning {} chain from height: {}'.format(self.coin_name(), restore_height))
self.rpc_callback('rescanblockchain', [restore_height])
self.rpc_wallet('rescanblockchain', [restore_height])
# TODO: Remove workaround
# utxos = self.rpc_callback('listunspentblind', [1, 9999999, [sx_addr]])
# utxos = self.rpc_wallet('listunspentblind', [1, 9999999, [sx_addr]])
utxos = []
all_utxos = self.rpc_callback('listunspentblind', [1, 9999999])
all_utxos = self.rpc_wallet('listunspentblind', [1, 9999999])
for utxo in all_utxos:
if utxo.get('stealth_address', '_') == sx_addr:
utxos.append(utxo)
@@ -741,14 +752,14 @@ class PARTInterfaceBlind(PARTInterface):
[{'address': address_to, 'amount': self.format_amount(cb_swap_value), 'subfee': True}, ],
'', '', self._anon_tx_ring_size, 1, False,
{'conf_target': self._conf_target, 'inputs': inputs, 'show_fee': True}]
rv = self.rpc_callback('sendtypeto', params)
rv = self.rpc_wallet('sendtypeto', params)
return bytes.fromhex(rv['txid'])
def findTxnByHash(self, txid_hex):
# txindex is enabled for Particl
try:
rv = self.rpc_callback('getrawtransaction', [txid_hex, True])
rv = self.rpc('getrawtransaction', [txid_hex, True])
except Exception as ex:
self._log.debug('findTxnByHash getrawtransaction failed: {}'.format(txid_hex))
return None
@@ -759,7 +770,7 @@ class PARTInterfaceBlind(PARTInterface):
return None
def createRawFundedTransaction(self, addr_to: str, amount: int, sub_fee: bool = False, lock_unspents: bool = True) -> str:
txn = self.rpc_callback('createrawtransaction', [[], {addr_to: self.format_amount(amount)}])
txn = self.rpc_wallet('createrawtransaction', [[], {addr_to: self.format_amount(amount)}])
options = {
'lockUnspents': lock_unspents,
@@ -767,7 +778,7 @@ class PARTInterfaceBlind(PARTInterface):
}
if sub_fee:
options['subtractFeeFromOutputs'] = [0,]
return self.rpc_callback('fundrawtransactionfrom', ['blind', txn, options])['hex']
return self.rpc_wallet('fundrawtransactionfrom', ['blind', txn, options])['hex']
class PARTInterfaceAnon(PARTInterface):
@@ -801,7 +812,7 @@ class PARTInterfaceAnon(PARTInterface):
'', '', self._anon_tx_ring_size, 1, False,
{'conf_target': self._conf_target, 'blind_watchonly_visible': True}]
txid = self.rpc_callback('sendtypeto', params)
txid = self.rpc_wallet('sendtypeto', params)
return bytes.fromhex(txid)
def findTxB(self, kbv, Kbs, cb_swap_value, cb_block_confirmed, restore_height, bid_sender):
@@ -813,17 +824,17 @@ class PARTInterfaceAnon(PARTInterface):
if bid_sender:
cb_swap_value *= -1
else:
addr_info = self.rpc_callback('getaddressinfo', [sx_addr])
addr_info = self.rpc_wallet('getaddressinfo', [sx_addr])
if not addr_info['iswatchonly']:
wif_prefix = self.chainparams_network()['key_prefix']
wif_scan_key = toWIF(wif_prefix, kbv)
self.rpc_callback('importstealthaddress', [wif_scan_key, Kbs.hex()])
self.rpc_wallet('importstealthaddress', [wif_scan_key, Kbs.hex()])
self._log.info('Imported watch-only sx_addr: {}'.format(sx_addr))
self._log.info('Rescanning {} chain from height: {}'.format(self.coin_name(), restore_height))
self.rpc_callback('rescanblockchain', [restore_height])
self.rpc_wallet('rescanblockchain', [restore_height])
params = [{'include_watchonly': True, 'search': sx_addr}]
txns = self.rpc_callback('filtertransactions', params)
txns = self.rpc_wallet('filtertransactions', params)
if len(txns) == 1:
tx = txns[0]
@@ -833,7 +844,7 @@ class PARTInterfaceAnon(PARTInterface):
if make_int(tx['outputs'][0]['amount']) == cb_swap_value:
height = 0
if tx['confirmations'] > 0:
chain_height = self.rpc_callback('getblockcount')
chain_height = self.rpc('getblockcount')
height = chain_height - (tx['confirmations'] - 1)
return {'txid': tx['txid'], 'amount': cb_swap_value, 'height': height}
else:
@@ -845,17 +856,17 @@ class PARTInterfaceAnon(PARTInterface):
Kbv = self.getPubkey(kbv)
Kbs = self.getPubkey(kbs)
sx_addr = self.formatStealthAddress(Kbv, Kbs)
addr_info = self.rpc_callback('getaddressinfo', [sx_addr])
addr_info = self.rpc_wallet('getaddressinfo', [sx_addr])
if not addr_info['ismine']:
wif_prefix = self.chainparams_network()['key_prefix']
wif_scan_key = toWIF(wif_prefix, kbv)
wif_spend_key = toWIF(wif_prefix, kbs)
self.rpc_callback('importstealthaddress', [wif_scan_key, wif_spend_key])
self.rpc_wallet('importstealthaddress', [wif_scan_key, wif_spend_key])
self._log.info('Imported spend key for sx_addr: {}'.format(sx_addr))
self._log.info('Rescanning {} chain from height: {}'.format(self.coin_name(), restore_height))
self.rpc_callback('rescanblockchain', [restore_height])
self.rpc_wallet('rescanblockchain', [restore_height])
autxos = self.rpc_callback('listunspentanon', [1, 9999999, [sx_addr]])
autxos = self.rpc_wallet('listunspentanon', [1, 9999999, [sx_addr]])
if len(autxos) < 1:
raise TemporaryError('No spendable outputs')
@@ -874,14 +885,14 @@ class PARTInterfaceAnon(PARTInterface):
[{'address': address_to, 'amount': self.format_amount(cb_swap_value), 'subfee': True}, ],
'', '', self._anon_tx_ring_size, 1, False,
{'conf_target': self._conf_target, 'inputs': inputs, 'show_fee': True}]
rv = self.rpc_callback('sendtypeto', params)
rv = self.rpc_wallet('sendtypeto', params)
return bytes.fromhex(rv['txid'])
def findTxnByHash(self, txid_hex: str):
# txindex is enabled for Particl
try:
rv = self.rpc_callback('getrawtransaction', [txid_hex, True])
rv = self.rpc('getrawtransaction', [txid_hex, True])
except Exception as ex:
self._log.debug('findTxnByHash getrawtransaction failed: {}'.format(txid_hex))
return None
@@ -892,4 +903,4 @@ class PARTInterfaceAnon(PARTInterface):
return None
def getSpendableBalance(self) -> int:
return self.make_int(self.rpc_callback('getbalances')['mine']['anon_trusted'])
return self.make_int(self.rpc_wallet('getbalances')['mine']['anon_trusted'])

View File

@@ -8,6 +8,7 @@
from io import BytesIO
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 (
@@ -29,12 +30,20 @@ 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)
# No multiwallet support
self.rpc_wallet = make_rpc_func(self._rpcport, self._rpcauth, host=self._rpc_host)
def checkWallets(self) -> int:
return 1
def signTxWithWallet(self, tx):
rv = self.rpc_callback('signrawtransaction', [tx.hex()])
rv = self.rpc('signrawtransaction', [tx.hex()])
return bytes.fromhex(rv['hex'])
def createRawFundedTransaction(self, addr_to: str, amount: int, sub_fee: bool = False, lock_unspents: bool = True) -> str:
txn = self.rpc_callback('createrawtransaction', [[], {addr_to: self.format_amount(amount)}])
txn = self.rpc('createrawtransaction', [[], {addr_to: self.format_amount(amount)}])
fee_rate, fee_src = self.get_fee_rate(self._conf_target)
self._log.debug(f'Fee rate: {fee_rate}, source: {fee_src}, block target: {self._conf_target}')
options = {
@@ -43,25 +52,25 @@ class PIVXInterface(BTCInterface):
}
if sub_fee:
options['subtractFeeFromOutputs'] = [0,]
return self.rpc_callback('fundrawtransaction', [txn, options])['hex']
return self.rpc('fundrawtransaction', [txn, options])['hex']
def createRawSignedTransaction(self, addr_to, amount) -> str:
txn_funded = self.createRawFundedTransaction(addr_to, amount)
return self.rpc_callback('signrawtransaction', [txn_funded])['hex']
return self.rpc('signrawtransaction', [txn_funded])['hex']
def decodeAddress(self, address):
return decodeAddress(address)[1:]
def getBlockWithTxns(self, block_hash):
# TODO: Bypass decoderawtransaction and getblockheader
block = self.rpc_callback('getblock', [block_hash, False])
block_header = self.rpc_callback('getblockheader', [block_hash])
block = self.rpc('getblock', [block_hash, False])
block_header = self.rpc('getblockheader', [block_hash])
decoded_block = CBlock()
decoded_block = FromHex(decoded_block, block)
tx_rv = []
for tx in decoded_block.vtx:
tx_dec = self.rpc_callback('decoderawtransaction', [ToHex(tx)])
tx_dec = self.rpc('decoderawtransaction', [ToHex(tx)])
tx_rv.append(tx_dec)
block_rv = {
@@ -77,10 +86,10 @@ class PIVXInterface(BTCInterface):
def withdrawCoin(self, value, addr_to, subfee):
params = [addr_to, value, '', '', subfee]
return self.rpc_callback('sendtoaddress', params)
return self.rpc('sendtoaddress', params)
def getSpendableBalance(self) -> int:
return self.make_int(self.rpc_callback('getwalletinfo')['balance'])
return self.make_int(self.rpc('getwalletinfo')['balance'])
def loadTx(self, tx_bytes):
# Load tx from bytes to internal representation
@@ -101,13 +110,13 @@ class PIVXInterface(BTCInterface):
def signTxWithKey(self, tx: bytes, key: bytes) -> bytes:
key_wif = self.encodeKey(key)
rv = self.rpc_callback('signrawtransaction', [tx.hex(), [], [key_wif, ]])
rv = self.rpc('signrawtransaction', [tx.hex(), [], [key_wif, ]])
return bytes.fromhex(rv['hex'])
def findTxnByHash(self, txid_hex: str):
# Only works for wallet txns
try:
rv = self.rpc_callback('gettransaction', [txid_hex])
rv = self.rpc('gettransaction', [txid_hex])
except Exception as ex:
self._log.debug('findTxnByHash getrawtransaction failed: {}'.format(txid_hex))
return None

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (c) 2020-2023 tecnovert
# Copyright (c) 2020-2024 tecnovert
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -27,16 +27,16 @@ from coincurve.dleag import (
from basicswap.interface import (
Curves)
from basicswap.util import (
i2b,
i2b, b2i, b2h,
dumpj,
ensure,
make_int,
TemporaryError)
from basicswap.util.network import (
is_private_ip_address)
from basicswap.rpc_xmr import (
make_xmr_rpc_func,
make_xmr_rpc2_func)
from basicswap.util import (
b2i, b2h)
from basicswap.chainparams import XMR_COIN, CoinInterface, Coins
@@ -80,12 +80,6 @@ class XMRInterface(CoinInterface):
def __init__(self, coin_settings, network, swap_client=None):
super().__init__(network)
daemon_login = None
if coin_settings.get('rpcuser', '') != '':
daemon_login = (coin_settings.get('rpcuser', ''), coin_settings.get('rpcpassword', ''))
self.rpc_cb = make_xmr_rpc_func(coin_settings['rpcport'], daemon_login, host=coin_settings.get('rpchost', '127.0.0.1'))
self.rpc_cb2 = make_xmr_rpc2_func(coin_settings['rpcport'], daemon_login, host=coin_settings.get('rpchost', '127.0.0.1')) # non-json endpoint
self.rpc_wallet_cb = make_xmr_rpc_func(coin_settings['walletrpcport'], coin_settings['walletrpcauth'], host=coin_settings.get('walletrpchost', '127.0.0.1'))
self.blocks_confirmed = coin_settings['blocks_confirmed']
self._restore_height = coin_settings.get('restore_height', 0)
@@ -95,6 +89,46 @@ class XMRInterface(CoinInterface):
self._wallet_password = None
self._have_checked_seed = False
daemon_login = None
if coin_settings.get('rpcuser', '') != '':
daemon_login = (coin_settings.get('rpcuser', ''), coin_settings.get('rpcpassword', ''))
rpchost = coin_settings.get('rpchost', '127.0.0.1')
proxy_host = None
proxy_port = None
# Connect to the daemon over a proxy if not running locally
if swap_client:
chain_client_settings = swap_client.getChainClientSettings(self.coin_type())
manage_daemon: bool = chain_client_settings['manage_daemon']
if swap_client.use_tor_proxy:
if manage_daemon is False:
log_str: str = ''
have_cc_tor_opt = 'use_tor' in chain_client_settings
if have_cc_tor_opt and chain_client_settings['use_tor'] is False:
log_str = ' bypassing proxy (use_tor false for XMR)'
elif have_cc_tor_opt is False and is_private_ip_address(rpchost):
log_str = ' bypassing proxy (private ip address)'
else:
proxy_host = swap_client.tor_proxy_host
proxy_port = swap_client.tor_proxy_port
log_str = f' through proxy at {proxy_host}'
self._log.info(f'Connecting to remote {self.coin_name()} daemon at {rpchost}{log_str}.')
else:
self._log.info(f'Not connecting to local {self.coin_name()} daemon through proxy.')
elif manage_daemon is False:
self._log.info(f'Connecting to remote {self.coin_name()} daemon at {rpchost}.')
self._rpctimeout = coin_settings.get('rpctimeout', 60)
self._walletrpctimeout = coin_settings.get('walletrpctimeout', 120)
self._walletrpctimeoutlong = coin_settings.get('walletrpctimeoutlong', 600)
self.rpc = make_xmr_rpc_func(coin_settings['rpcport'], daemon_login, host=rpchost, proxy_host=proxy_host, proxy_port=proxy_port, default_timeout=self._rpctimeout, tag='Node(j) ')
self.rpc2 = make_xmr_rpc2_func(coin_settings['rpcport'], daemon_login, host=rpchost, proxy_host=proxy_host, proxy_port=proxy_port, default_timeout=self._rpctimeout, tag='Node ') # non-json endpoint
self.rpc_wallet = make_xmr_rpc_func(coin_settings['walletrpcport'], coin_settings['walletrpcauth'], host=coin_settings.get('walletrpchost', '127.0.0.1'), default_timeout=self._walletrpctimeout, tag='Wallet ')
def checkWallets(self) -> int:
return 1
def setFeePriority(self, new_priority):
ensure(new_priority >= 0 and new_priority < 4, 'Invalid fee_priority value')
self._fee_priority = new_priority
@@ -105,7 +139,7 @@ class XMRInterface(CoinInterface):
def createWallet(self, params):
if self._wallet_password is not None:
params['password'] = self._wallet_password
rv = self.rpc_wallet_cb('generate_from_keys', params)
rv = self.rpc_wallet('generate_from_keys', params)
self._log.info('generate_from_keys %s', dumpj(rv))
def openWallet(self, filename):
@@ -115,10 +149,10 @@ class XMRInterface(CoinInterface):
try:
# Can't reopen the same wallet in windows, !is_keys_file_locked()
self.rpc_wallet_cb('close_wallet')
self.rpc_wallet('close_wallet')
except Exception:
pass
self.rpc_wallet_cb('open_wallet', params)
self.rpc_wallet('open_wallet', params)
def initialiseWallet(self, key_view, key_spend, restore_height=None):
with self._mx_wallet:
@@ -147,14 +181,14 @@ class XMRInterface(CoinInterface):
with self._mx_wallet:
self.openWallet(self._wallet_filename)
def testDaemonRPC(self, with_wallet=True):
self.rpc_wallet_cb('get_languages')
def testDaemonRPC(self, with_wallet=True) -> None:
self.rpc_wallet('get_languages')
def getDaemonVersion(self):
return self.rpc_wallet_cb('get_version')['version']
return self.rpc_wallet('get_version')['version']
def getBlockchainInfo(self):
get_height = self.rpc_cb2('get_height', timeout=30)
get_height = self.rpc2('get_height', timeout=self._rpctimeout)
rv = {
'blocks': get_height['height'],
'verificationprogress': 0.0,
@@ -165,7 +199,7 @@ class XMRInterface(CoinInterface):
# get_block_count returns "Internal error" if bootstrap-daemon is active
if get_height['untrusted'] is True:
rv['bootstrapping'] = True
get_info = self.rpc_cb2('get_info', timeout=30)
get_info = self.rpc2('get_info', timeout=self._rpctimeout)
if 'height_without_bootstrap' in get_info:
rv['blocks'] = get_info['height_without_bootstrap']
@@ -173,7 +207,7 @@ class XMRInterface(CoinInterface):
if rv['known_block_count'] > rv['blocks']:
rv['verificationprogress'] = rv['blocks'] / rv['known_block_count']
else:
rv['known_block_count'] = self.rpc_cb('get_block_count', timeout=30)['count']
rv['known_block_count'] = self.rpc('get_block_count', timeout=self._rpctimeout)['count']
rv['verificationprogress'] = rv['blocks'] / rv['known_block_count']
except Exception as e:
self._log.warning('XMR get_block_count failed with: %s', str(e))
@@ -182,7 +216,7 @@ class XMRInterface(CoinInterface):
return rv
def getChainHeight(self):
return self.rpc_cb2('get_height', timeout=30)['height']
return self.rpc2('get_height', timeout=self._rpctimeout)['height']
def getWalletInfo(self):
with self._mx_wallet:
@@ -195,8 +229,8 @@ class XMRInterface(CoinInterface):
raise e
rv = {}
self.rpc_wallet_cb('refresh')
balance_info = self.rpc_wallet_cb('get_balance')
self.rpc_wallet('refresh')
balance_info = self.rpc_wallet('get_balance')
rv['balance'] = self.format_amount(balance_info['unlocked_balance'])
rv['unconfirmed_balance'] = self.format_amount(balance_info['balance'] - balance_info['unlocked_balance'])
rv['encrypted'] = False if self._wallet_password is None else True
@@ -209,17 +243,17 @@ class XMRInterface(CoinInterface):
def getMainWalletAddress(self) -> str:
with self._mx_wallet:
self.openWallet(self._wallet_filename)
return self.rpc_wallet_cb('get_address')['address']
return self.rpc_wallet('get_address')['address']
def getNewAddress(self, placeholder) -> str:
with self._mx_wallet:
self.openWallet(self._wallet_filename)
new_address = self.rpc_wallet_cb('create_address', {'account_index': 0})['address']
self.rpc_wallet_cb('store')
new_address = self.rpc_wallet('create_address', {'account_index': 0})['address']
self.rpc_wallet('store')
return new_address
def get_fee_rate(self, conf_target: int = 2):
self._log.warning('TODO - estimate fee rate?')
self._log.warning('TODO - estimate XMR fee rate?')
return 0.0, 'unused'
def getNewSecretKey(self) -> bytes:
@@ -280,7 +314,7 @@ class XMRInterface(CoinInterface):
def publishBLockTx(self, kbv: bytes, Kbs: bytes, output_amount: int, feerate: int, delay_for: int = 10, unlock_time: int = 0) -> bytes:
with self._mx_wallet:
self.openWallet(self._wallet_filename)
self.rpc_wallet_cb('refresh')
self.rpc_wallet('refresh')
Kbv = self.getPubkey(kbv)
shared_addr = xmr_util.encode_address(Kbv, Kbs)
@@ -288,7 +322,7 @@ class XMRInterface(CoinInterface):
params = {'destinations': [{'amount': output_amount, 'address': shared_addr}], 'unlock_time': unlock_time}
if self._fee_priority > 0:
params['priority'] = self._fee_priority
rv = self.rpc_wallet_cb('transfer', params)
rv = self.rpc_wallet('transfer', params)
self._log.info('publishBLockTx %s to address_b58 %s', rv['tx_hash'], shared_addr)
tx_hash = bytes.fromhex(rv['tx_hash'])
@@ -296,7 +330,7 @@ class XMRInterface(CoinInterface):
i = 0
while not self._sc.delay_event.is_set():
gt_params = {'out': True, 'pending': True, 'failed': True, 'pool': True, }
rv = self.rpc_wallet_cb('get_transfers', gt_params)
rv = self.rpc_wallet('get_transfers', gt_params)
self._log.debug('get_transfers {}'.format(dumpj(rv)))
if 'pending' not in rv:
break
@@ -325,26 +359,26 @@ class XMRInterface(CoinInterface):
self.createWallet(params)
self.openWallet(address_b58)
self.rpc_wallet_cb('refresh', timeout=600)
self.rpc_wallet('refresh', timeout=self._walletrpctimeoutlong)
'''
# Debug
try:
current_height = self.rpc_wallet_cb('get_height')['height']
current_height = self.rpc_wallet('get_height')['height']
self._log.info('findTxB XMR current_height %d\nAddress: %s', current_height, address_b58)
except Exception as e:
self._log.info('rpc_cb failed %s', str(e))
self._log.info('rpc failed %s', str(e))
current_height = None # If the transfer is available it will be deep enough
# and (current_height is None or current_height - transfer['block_height'] > cb_block_confirmed):
'''
params = {'transfer_type': 'available'}
transfers = self.rpc_wallet_cb('incoming_transfers', params)
transfers = self.rpc_wallet('incoming_transfers', params)
rv = None
if 'transfers' in transfers:
for transfer in transfers['transfers']:
# unlocked <- wallet->is_transfer_unlocked() checks unlock_time and CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE
if not transfer['unlocked']:
full_tx = self.rpc_wallet_cb('get_transfer_by_txid', {'txid': transfer['tx_hash']})
full_tx = self.rpc_wallet('get_transfer_by_txid', {'txid': transfer['tx_hash']})
unlock_time = full_tx['transfer']['unlock_time']
if unlock_time != 0:
self._log.warning('Coin b lock txn is locked: {}, unlock_time {}'.format(transfer['tx_hash'], unlock_time))
@@ -360,17 +394,17 @@ class XMRInterface(CoinInterface):
def findTxnByHash(self, txid):
with self._mx_wallet:
self.openWallet(self._wallet_filename)
self.rpc_wallet_cb('refresh', timeout=600)
self.rpc_wallet('refresh', timeout=self._walletrpctimeoutlong)
try:
current_height = self.rpc_cb2('get_height', timeout=30)['height']
current_height = self.rpc2('get_height', timeout=self._rpctimeout)['height']
self._log.info('findTxnByHash XMR current_height %d\nhash: %s', current_height, txid)
except Exception as e:
self._log.info('rpc_cb failed %s', str(e))
self._log.info('rpc failed %s', str(e))
current_height = None # If the transfer is available it will be deep enough
params = {'transfer_type': 'available'}
rv = self.rpc_wallet_cb('incoming_transfers', params)
rv = self.rpc_wallet('incoming_transfers', params)
if 'transfers' in rv:
for transfer in rv['transfers']:
if transfer['tx_hash'] == txid \
@@ -405,11 +439,11 @@ class XMRInterface(CoinInterface):
self.createWallet(params)
self.openWallet(wallet_filename)
self.rpc_wallet_cb('refresh')
rv = self.rpc_wallet_cb('get_balance')
self.rpc_wallet('refresh')
rv = self.rpc_wallet('get_balance')
if rv['balance'] < cb_swap_value:
self._log.warning('Balance is too low, checking for existing spend.')
txns = self.rpc_wallet_cb('get_transfers', {'out': True})
txns = self.rpc_wallet('get_transfers', {'out': True})
if 'out' in txns:
txns = txns['out']
if len(txns) > 0:
@@ -434,7 +468,7 @@ class XMRInterface(CoinInterface):
if self._fee_priority > 0:
params['priority'] = self._fee_priority
rv = self.rpc_wallet_cb('sweep_all', params)
rv = self.rpc_wallet('sweep_all', params)
self._log.debug('sweep_all {}'.format(json.dumps(rv)))
return bytes.fromhex(rv['tx_hash_list'][0])
@@ -444,24 +478,24 @@ class XMRInterface(CoinInterface):
value_sats = make_int(value, self.exp())
self.openWallet(self._wallet_filename)
self.rpc_wallet_cb('refresh')
self.rpc_wallet('refresh')
if subfee:
balance = self.rpc_wallet_cb('get_balance')
balance = self.rpc_wallet('get_balance')
diff = balance['unlocked_balance'] - value_sats
if diff >= 0 and diff <= 10:
self._log.info('subfee enabled and value close to total, using sweep_all.')
params = {'address': addr_to}
if self._fee_priority > 0:
params['priority'] = self._fee_priority
rv = self.rpc_wallet_cb('sweep_all', params)
rv = self.rpc_wallet('sweep_all', params)
return rv['tx_hash_list'][0]
raise ValueError('Withdraw value must be close to total to use subfee/sweep_all.')
params = {'destinations': [{'amount': value_sats, 'address': addr_to}]}
if self._fee_priority > 0:
params['priority'] = self._fee_priority
rv = self.rpc_wallet_cb('transfer', params)
rv = self.rpc_wallet('transfer', params)
return rv['tx_hash']
def showLockTransfers(self, kbv, Kbs, restore_height):
@@ -488,9 +522,9 @@ class XMRInterface(CoinInterface):
self.createWallet(params)
self.openWallet(address_b58)
self.rpc_wallet_cb('refresh')
self.rpc_wallet('refresh')
rv = self.rpc_wallet_cb('get_transfers', {'in': True, 'out': True, 'pending': True, 'failed': True})
rv = self.rpc_wallet('get_transfers', {'in': True, 'out': True, 'pending': True, 'failed': True})
rv['filename'] = wallet_file
return rv
except Exception as e:
@@ -500,8 +534,8 @@ class XMRInterface(CoinInterface):
with self._mx_wallet:
self.openWallet(self._wallet_filename)
self.rpc_wallet_cb('refresh')
balance_info = self.rpc_wallet_cb('get_balance')
self.rpc_wallet('refresh')
balance_info = self.rpc_wallet('get_balance')
return balance_info['unlocked_balance']
def changeWalletPassword(self, old_password, new_password):
@@ -511,7 +545,7 @@ class XMRInterface(CoinInterface):
self._wallet_password = old_password
try:
self.openWallet(self._wallet_filename)
self.rpc_wallet_cb('change_wallet_password', {'old_password': old_password, 'new_password': new_password})
self.rpc_wallet('change_wallet_password', {'old_password': old_password, 'new_password': new_password})
except Exception as e:
self._wallet_password = orig_password
raise e
@@ -536,4 +570,4 @@ class XMRInterface(CoinInterface):
raise ValueError('Balance too low')
def getTransaction(self, txid: bytes):
return self.rpc_cb2('get_transactions', {'txs_hashes': [txid.hex(), ]})
return self.rpc2('get_transactions', {'txs_hashes': [txid.hex(), ]})

View File

@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020-2023 tecnovert
# Copyright (c) 2020-2024 tecnovert
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -63,6 +63,9 @@ def withdraw_coin(swap_client, coin_type, post_string, is_json):
type_from = get_data_entry_or(post_data, 'type_from', 'plain')
type_to = get_data_entry_or(post_data, 'type_to', 'plain')
txid_hex = swap_client.withdrawParticl(type_from, type_to, value, address, subfee)
elif coin_type == Coins.LTC:
type_from = get_data_entry_or(post_data, 'type_from', 'plain')
txid_hex = swap_client.withdrawLTC(type_from, value, address, subfee)
else:
txid_hex = swap_client.withdrawCoin(coin_type, value, address, subfee)
@@ -81,17 +84,22 @@ def js_coins(self, url_split, post_string, is_json) -> bytes:
for coin in Coins:
cc = swap_client.coin_clients[coin]
coin_chainparams = chainparams[cc['coin']]
coin_active: bool = False if cc['connection_type'] == 'none' else True
if coin == Coins.LTC_MWEB:
coin_active = False
entry = {
'id': int(coin),
'ticker': coin_chainparams['ticker'],
'name': getCoinName(coin),
'active': False if cc['connection_type'] == 'none' else True,
'active': coin_active,
'decimal_places': coin_chainparams['decimal_places'],
}
if coin == Coins.PART_ANON:
entry['variant'] = 'Anon'
elif coin == Coins.PART_BLIND:
entry['variant'] = 'Blind'
elif coin == Coins.LTC_MWEB:
entry['variant'] = 'MWEB'
coins.append(entry)
return bytes(json.dumps(coins), 'UTF-8')
@@ -108,19 +116,30 @@ def js_wallets(self, url_split, post_string, is_json):
cmd = url_split[4]
if cmd == 'withdraw':
return bytes(json.dumps(withdraw_coin(swap_client, coin_type, post_string, is_json)), 'UTF-8')
if cmd == 'nextdepositaddr':
elif cmd == 'nextdepositaddr':
return bytes(json.dumps(swap_client.cacheNewAddressForCoin(coin_type)), 'UTF-8')
if cmd == 'createutxo':
elif cmd == 'createutxo':
post_data = getFormData(post_string, is_json)
ci = swap_client.ci(coin_type)
value = ci.make_int(get_data_entry(post_data, 'value'))
txid_hex, new_addr = ci.createUTXO(value)
return bytes(json.dumps({'txid': txid_hex, 'address': new_addr}), 'UTF-8')
if cmd == 'reseed':
elif cmd == 'reseed':
swap_client.reseedWallet(coin_type)
return bytes(json.dumps({'reseeded': True}), 'UTF-8')
elif cmd == 'newstealthaddress':
if coin_type != Coins.PART:
raise ValueError('Invalid coin for command')
return bytes(json.dumps(swap_client.ci(coin_type).getNewStealthAddress()), 'UTF-8')
elif cmd == 'newmwebaddress':
if coin_type not in (Coins.LTC, Coins.LTC_MWEB):
raise ValueError('Invalid coin for command')
return bytes(json.dumps(swap_client.ci(coin_type).getNewMwebAddress()), 'UTF-8')
raise ValueError('Unknown command')
if coin_type == Coins.LTC_MWEB:
coin_type = Coins.LTC
rv = swap_client.getWalletInfo(coin_type)
rv.update(swap_client.getBlockchainInfo(coin_type))
ci = swap_client.ci(coin_type)
@@ -647,7 +666,7 @@ def js_setpassword(self, url_split, post_string, is_json) -> bytes:
if have_data_entry(post_data, 'coin'):
# Set password for one coin
coin = getCoinType(get_data_entry(post_data, 'coin'))
if coin in (Coins.PART_ANON, Coins.PART_BLIND):
if coin in (Coins.PART_ANON, Coins.PART_BLIND, Coins.LTC_MWEB):
raise ValueError('Invalid coin.')
swap_client.changeWalletPasswords(old_password, new_password, coin)
return bytes(json.dumps({'success': True}), 'UTF-8')

View File

@@ -33,6 +33,8 @@ message OfferMessage {
uint32 protocol_version = 16;
bool amount_negotiable = 17;
bool rate_negotiable = 18;
bytes proof_utxos = 19; /* 32 byte txid 2 byte vout, repeated */
}
/* Step 2, buyer -> seller */
@@ -46,8 +48,25 @@ message BidMessage {
string proof_signature = 7;
uint32 protocol_version = 8;
bytes proof_utxos = 9; /* 32 byte txid 2 byte vout, repeated */
}
/* For tests */
message BidMessage_v1Deprecated {
bytes offer_msg_id = 1;
uint64 time_valid = 2; /* seconds bid is valid for */
uint64 amount = 3; /* amount of amount_from bid is for */
uint64 rate = 4;
bytes pkhash_buyer = 5; /* buyer's address to receive amount_from */
string proof_address = 6;
string proof_signature = 7;
uint32 protocol_version = 8;
}
/* Step 3, seller -> buyer */
message BidAcceptMessage {
bytes bid_msg_id = 1;

View File

@@ -2,10 +2,10 @@
# Generated by the protocol buffer compiler. DO NOT EDIT!
# source: messages.proto
"""Generated protocol buffer code."""
from google.protobuf.internal import builder as _builder
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf import symbol_database as _symbol_database
from google.protobuf.internal import builder as _builder
# @@protoc_insertion_point(imports)
_sym_db = _symbol_database.Default()
@@ -13,39 +13,41 @@ _sym_db = _symbol_database.Default()
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0emessages.proto\x12\tbasicswap\"\xa6\x04\n\x0cOfferMessage\x12\x11\n\tcoin_from\x18\x01 \x01(\r\x12\x0f\n\x07\x63oin_to\x18\x02 \x01(\r\x12\x13\n\x0b\x61mount_from\x18\x03 \x01(\x04\x12\x0c\n\x04rate\x18\x04 \x01(\x04\x12\x16\n\x0emin_bid_amount\x18\x05 \x01(\x04\x12\x12\n\ntime_valid\x18\x06 \x01(\x04\x12\x33\n\tlock_type\x18\x07 \x01(\x0e\x32 .basicswap.OfferMessage.LockType\x12\x12\n\nlock_value\x18\x08 \x01(\r\x12\x11\n\tswap_type\x18\t \x01(\r\x12\x15\n\rproof_address\x18\n \x01(\t\x12\x17\n\x0fproof_signature\x18\x0b \x01(\t\x12\x15\n\rpkhash_seller\x18\x0c \x01(\x0c\x12\x13\n\x0bsecret_hash\x18\r \x01(\x0c\x12\x15\n\rfee_rate_from\x18\x0e \x01(\x04\x12\x13\n\x0b\x66\x65\x65_rate_to\x18\x0f \x01(\x04\x12\x18\n\x10protocol_version\x18\x10 \x01(\r\x12\x19\n\x11\x61mount_negotiable\x18\x11 \x01(\x08\x12\x17\n\x0frate_negotiable\x18\x12 \x01(\x08\"q\n\x08LockType\x12\x0b\n\x07NOT_SET\x10\x00\x12\x18\n\x14SEQUENCE_LOCK_BLOCKS\x10\x01\x12\x16\n\x12SEQUENCE_LOCK_TIME\x10\x02\x12\x13\n\x0f\x41\x42S_LOCK_BLOCKS\x10\x03\x12\x11\n\rABS_LOCK_TIME\x10\x04\"\xb4\x01\n\nBidMessage\x12\x14\n\x0coffer_msg_id\x18\x01 \x01(\x0c\x12\x12\n\ntime_valid\x18\x02 \x01(\x04\x12\x0e\n\x06\x61mount\x18\x03 \x01(\x04\x12\x0c\n\x04rate\x18\x04 \x01(\x04\x12\x14\n\x0cpkhash_buyer\x18\x05 \x01(\x0c\x12\x15\n\rproof_address\x18\x06 \x01(\t\x12\x17\n\x0fproof_signature\x18\x07 \x01(\t\x12\x18\n\x10protocol_version\x18\x08 \x01(\r\"V\n\x10\x42idAcceptMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x15\n\rinitiate_txid\x18\x02 \x01(\x0c\x12\x17\n\x0f\x63ontract_script\x18\x03 \x01(\x0c\"=\n\x12OfferRevokeMessage\x12\x14\n\x0coffer_msg_id\x18\x01 \x01(\x0c\x12\x11\n\tsignature\x18\x02 \x01(\x0c\";\n\x10\x42idRejectMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x13\n\x0breject_code\x18\x02 \x01(\r\"\xb2\x01\n\rXmrBidMessage\x12\x14\n\x0coffer_msg_id\x18\x01 \x01(\x0c\x12\x12\n\ntime_valid\x18\x02 \x01(\x04\x12\x0e\n\x06\x61mount\x18\x03 \x01(\x04\x12\x0c\n\x04rate\x18\x04 \x01(\x04\x12\x0c\n\x04pkaf\x18\x05 \x01(\x0c\x12\x0c\n\x04kbvf\x18\x06 \x01(\x0c\x12\x12\n\nkbsf_dleag\x18\x07 \x01(\x0c\x12\x0f\n\x07\x64\x65st_af\x18\x08 \x01(\x0c\x12\x18\n\x10protocol_version\x18\t \x01(\r\"T\n\x0fXmrSplitMessage\x12\x0e\n\x06msg_id\x18\x01 \x01(\x0c\x12\x10\n\x08msg_type\x18\x02 \x01(\r\x12\x10\n\x08sequence\x18\x03 \x01(\r\x12\r\n\x05\x64leag\x18\x04 \x01(\x0c\"\x80\x02\n\x13XmrBidAcceptMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x0c\n\x04pkal\x18\x03 \x01(\x0c\x12\x0c\n\x04kbvl\x18\x04 \x01(\x0c\x12\x12\n\nkbsl_dleag\x18\x05 \x01(\x0c\x12\x11\n\ta_lock_tx\x18\x06 \x01(\x0c\x12\x18\n\x10\x61_lock_tx_script\x18\x07 \x01(\x0c\x12\x18\n\x10\x61_lock_refund_tx\x18\x08 \x01(\x0c\x12\x1f\n\x17\x61_lock_refund_tx_script\x18\t \x01(\x0c\x12\x1e\n\x16\x61_lock_refund_spend_tx\x18\n \x01(\x0c\x12\x1d\n\x15\x61l_lock_refund_tx_sig\x18\x0b \x01(\x0c\"r\n\x17XmrBidLockTxSigsMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12$\n\x1c\x61\x66_lock_refund_spend_tx_esig\x18\x02 \x01(\x0c\x12\x1d\n\x15\x61\x66_lock_refund_tx_sig\x18\x03 \x01(\x0c\"X\n\x18XmrBidLockSpendTxMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x17\n\x0f\x61_lock_spend_tx\x18\x02 \x01(\x0c\x12\x0f\n\x07kal_sig\x18\x03 \x01(\x0c\"M\n\x18XmrBidLockReleaseMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x1d\n\x15\x61l_lock_spend_tx_esig\x18\x02 \x01(\x0c\"\x8f\x01\n\x13\x41\x44SBidIntentMessage\x12\x14\n\x0coffer_msg_id\x18\x01 \x01(\x0c\x12\x12\n\ntime_valid\x18\x02 \x01(\x04\x12\x13\n\x0b\x61mount_from\x18\x03 \x01(\x04\x12\x11\n\tamount_to\x18\x04 \x01(\x04\x12\x0c\n\x04rate\x18\x05 \x01(\x04\x12\x18\n\x10protocol_version\x18\x06 \x01(\r\"p\n\x19\x41\x44SBidIntentAcceptMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x0c\n\x04pkaf\x18\x02 \x01(\x0c\x12\x0c\n\x04kbvf\x18\x03 \x01(\x0c\x12\x12\n\nkbsf_dleag\x18\x04 \x01(\x0c\x12\x0f\n\x07\x64\x65st_af\x18\x05 \x01(\x0c\x62\x06proto3')
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0emessages.proto\x12\tbasicswap\"\xa6\x04\n\x0cOfferMessage\x12\x11\n\tcoin_from\x18\x01 \x01(\r\x12\x0f\n\x07\x63oin_to\x18\x02 \x01(\r\x12\x13\n\x0b\x61mount_from\x18\x03 \x01(\x04\x12\x0c\n\x04rate\x18\x04 \x01(\x04\x12\x16\n\x0emin_bid_amount\x18\x05 \x01(\x04\x12\x12\n\ntime_valid\x18\x06 \x01(\x04\x12\x33\n\tlock_type\x18\x07 \x01(\x0e\x32 .basicswap.OfferMessage.LockType\x12\x12\n\nlock_value\x18\x08 \x01(\r\x12\x11\n\tswap_type\x18\t \x01(\r\x12\x15\n\rproof_address\x18\n \x01(\t\x12\x17\n\x0fproof_signature\x18\x0b \x01(\t\x12\x15\n\rpkhash_seller\x18\x0c \x01(\x0c\x12\x13\n\x0bsecret_hash\x18\r \x01(\x0c\x12\x15\n\rfee_rate_from\x18\x0e \x01(\x04\x12\x13\n\x0b\x66\x65\x65_rate_to\x18\x0f \x01(\x04\x12\x18\n\x10protocol_version\x18\x10 \x01(\r\x12\x19\n\x11\x61mount_negotiable\x18\x11 \x01(\x08\x12\x17\n\x0frate_negotiable\x18\x12 \x01(\x08\"q\n\x08LockType\x12\x0b\n\x07NOT_SET\x10\x00\x12\x18\n\x14SEQUENCE_LOCK_BLOCKS\x10\x01\x12\x16\n\x12SEQUENCE_LOCK_TIME\x10\x02\x12\x13\n\x0f\x41\x42S_LOCK_BLOCKS\x10\x03\x12\x11\n\rABS_LOCK_TIME\x10\x04\"\xc9\x01\n\nBidMessage\x12\x14\n\x0coffer_msg_id\x18\x01 \x01(\x0c\x12\x12\n\ntime_valid\x18\x02 \x01(\x04\x12\x0e\n\x06\x61mount\x18\x03 \x01(\x04\x12\x0c\n\x04rate\x18\x04 \x01(\x04\x12\x14\n\x0cpkhash_buyer\x18\x05 \x01(\x0c\x12\x15\n\rproof_address\x18\x06 \x01(\t\x12\x17\n\x0fproof_signature\x18\x07 \x01(\t\x12\x18\n\x10protocol_version\x18\x08 \x01(\r\x12\x13\n\x0bproof_utxos\x18\t \x01(\x0c\"\xc1\x01\n\x17\x42idMessage_v1Deprecated\x12\x14\n\x0coffer_msg_id\x18\x01 \x01(\x0c\x12\x12\n\ntime_valid\x18\x02 \x01(\x04\x12\x0e\n\x06\x61mount\x18\x03 \x01(\x04\x12\x0c\n\x04rate\x18\x04 \x01(\x04\x12\x14\n\x0cpkhash_buyer\x18\x05 \x01(\x0c\x12\x15\n\rproof_address\x18\x06 \x01(\t\x12\x17\n\x0fproof_signature\x18\x07 \x01(\t\x12\x18\n\x10protocol_version\x18\x08 \x01(\r\"V\n\x10\x42idAcceptMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x15\n\rinitiate_txid\x18\x02 \x01(\x0c\x12\x17\n\x0f\x63ontract_script\x18\x03 \x01(\x0c\"=\n\x12OfferRevokeMessage\x12\x14\n\x0coffer_msg_id\x18\x01 \x01(\x0c\x12\x11\n\tsignature\x18\x02 \x01(\x0c\";\n\x10\x42idRejectMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x13\n\x0breject_code\x18\x02 \x01(\r\"\xb2\x01\n\rXmrBidMessage\x12\x14\n\x0coffer_msg_id\x18\x01 \x01(\x0c\x12\x12\n\ntime_valid\x18\x02 \x01(\x04\x12\x0e\n\x06\x61mount\x18\x03 \x01(\x04\x12\x0c\n\x04rate\x18\x04 \x01(\x04\x12\x0c\n\x04pkaf\x18\x05 \x01(\x0c\x12\x0c\n\x04kbvf\x18\x06 \x01(\x0c\x12\x12\n\nkbsf_dleag\x18\x07 \x01(\x0c\x12\x0f\n\x07\x64\x65st_af\x18\x08 \x01(\x0c\x12\x18\n\x10protocol_version\x18\t \x01(\r\"T\n\x0fXmrSplitMessage\x12\x0e\n\x06msg_id\x18\x01 \x01(\x0c\x12\x10\n\x08msg_type\x18\x02 \x01(\r\x12\x10\n\x08sequence\x18\x03 \x01(\r\x12\r\n\x05\x64leag\x18\x04 \x01(\x0c\"\x80\x02\n\x13XmrBidAcceptMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x0c\n\x04pkal\x18\x03 \x01(\x0c\x12\x0c\n\x04kbvl\x18\x04 \x01(\x0c\x12\x12\n\nkbsl_dleag\x18\x05 \x01(\x0c\x12\x11\n\ta_lock_tx\x18\x06 \x01(\x0c\x12\x18\n\x10\x61_lock_tx_script\x18\x07 \x01(\x0c\x12\x18\n\x10\x61_lock_refund_tx\x18\x08 \x01(\x0c\x12\x1f\n\x17\x61_lock_refund_tx_script\x18\t \x01(\x0c\x12\x1e\n\x16\x61_lock_refund_spend_tx\x18\n \x01(\x0c\x12\x1d\n\x15\x61l_lock_refund_tx_sig\x18\x0b \x01(\x0c\"r\n\x17XmrBidLockTxSigsMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12$\n\x1c\x61\x66_lock_refund_spend_tx_esig\x18\x02 \x01(\x0c\x12\x1d\n\x15\x61\x66_lock_refund_tx_sig\x18\x03 \x01(\x0c\"X\n\x18XmrBidLockSpendTxMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x17\n\x0f\x61_lock_spend_tx\x18\x02 \x01(\x0c\x12\x0f\n\x07kal_sig\x18\x03 \x01(\x0c\"M\n\x18XmrBidLockReleaseMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x1d\n\x15\x61l_lock_spend_tx_esig\x18\x02 \x01(\x0c\"\x8f\x01\n\x13\x41\x44SBidIntentMessage\x12\x14\n\x0coffer_msg_id\x18\x01 \x01(\x0c\x12\x12\n\ntime_valid\x18\x02 \x01(\x04\x12\x13\n\x0b\x61mount_from\x18\x03 \x01(\x04\x12\x11\n\tamount_to\x18\x04 \x01(\x04\x12\x0c\n\x04rate\x18\x05 \x01(\x04\x12\x18\n\x10protocol_version\x18\x06 \x01(\r\"p\n\x19\x41\x44SBidIntentAcceptMessage\x12\x12\n\nbid_msg_id\x18\x01 \x01(\x0c\x12\x0c\n\x04pkaf\x18\x02 \x01(\x0c\x12\x0c\n\x04kbvf\x18\x03 \x01(\x0c\x12\x12\n\nkbsf_dleag\x18\x04 \x01(\x0c\x12\x0f\n\x07\x64\x65st_af\x18\x05 \x01(\x0c\x62\x06proto3')
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'messages_pb2', globals())
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'messages_pb2', _globals)
if _descriptor._USE_C_DESCRIPTORS == False:
DESCRIPTOR._options = None
_OFFERMESSAGE._serialized_start=30
_OFFERMESSAGE._serialized_end=580
_OFFERMESSAGE_LOCKTYPE._serialized_start=467
_OFFERMESSAGE_LOCKTYPE._serialized_end=580
_BIDMESSAGE._serialized_start=583
_BIDMESSAGE._serialized_end=763
_BIDACCEPTMESSAGE._serialized_start=765
_BIDACCEPTMESSAGE._serialized_end=851
_OFFERREVOKEMESSAGE._serialized_start=853
_OFFERREVOKEMESSAGE._serialized_end=914
_BIDREJECTMESSAGE._serialized_start=916
_BIDREJECTMESSAGE._serialized_end=975
_XMRBIDMESSAGE._serialized_start=978
_XMRBIDMESSAGE._serialized_end=1156
_XMRSPLITMESSAGE._serialized_start=1158
_XMRSPLITMESSAGE._serialized_end=1242
_XMRBIDACCEPTMESSAGE._serialized_start=1245
_XMRBIDACCEPTMESSAGE._serialized_end=1501
_XMRBIDLOCKTXSIGSMESSAGE._serialized_start=1503
_XMRBIDLOCKTXSIGSMESSAGE._serialized_end=1617
_XMRBIDLOCKSPENDTXMESSAGE._serialized_start=1619
_XMRBIDLOCKSPENDTXMESSAGE._serialized_end=1707
_XMRBIDLOCKRELEASEMESSAGE._serialized_start=1709
_XMRBIDLOCKRELEASEMESSAGE._serialized_end=1786
_ADSBIDINTENTMESSAGE._serialized_start=1789
_ADSBIDINTENTMESSAGE._serialized_end=1932
_ADSBIDINTENTACCEPTMESSAGE._serialized_start=1934
_ADSBIDINTENTACCEPTMESSAGE._serialized_end=2046
_globals['_OFFERMESSAGE']._serialized_start=30
_globals['_OFFERMESSAGE']._serialized_end=580
_globals['_OFFERMESSAGE_LOCKTYPE']._serialized_start=467
_globals['_OFFERMESSAGE_LOCKTYPE']._serialized_end=580
_globals['_BIDMESSAGE']._serialized_start=583
_globals['_BIDMESSAGE']._serialized_end=784
_globals['_BIDMESSAGE_V1DEPRECATED']._serialized_start=787
_globals['_BIDMESSAGE_V1DEPRECATED']._serialized_end=980
_globals['_BIDACCEPTMESSAGE']._serialized_start=982
_globals['_BIDACCEPTMESSAGE']._serialized_end=1068
_globals['_OFFERREVOKEMESSAGE']._serialized_start=1070
_globals['_OFFERREVOKEMESSAGE']._serialized_end=1131
_globals['_BIDREJECTMESSAGE']._serialized_start=1133
_globals['_BIDREJECTMESSAGE']._serialized_end=1192
_globals['_XMRBIDMESSAGE']._serialized_start=1195
_globals['_XMRBIDMESSAGE']._serialized_end=1373
_globals['_XMRSPLITMESSAGE']._serialized_start=1375
_globals['_XMRSPLITMESSAGE']._serialized_end=1459
_globals['_XMRBIDACCEPTMESSAGE']._serialized_start=1462
_globals['_XMRBIDACCEPTMESSAGE']._serialized_end=1718
_globals['_XMRBIDLOCKTXSIGSMESSAGE']._serialized_start=1720
_globals['_XMRBIDLOCKTXSIGSMESSAGE']._serialized_end=1834
_globals['_XMRBIDLOCKSPENDTXMESSAGE']._serialized_start=1836
_globals['_XMRBIDLOCKSPENDTXMESSAGE']._serialized_end=1924
_globals['_XMRBIDLOCKRELEASEMESSAGE']._serialized_start=1926
_globals['_XMRBIDLOCKRELEASEMESSAGE']._serialized_end=2003
_globals['_ADSBIDINTENTMESSAGE']._serialized_start=2006
_globals['_ADSBIDINTENTMESSAGE']._serialized_end=2149
_globals['_ADSBIDINTENTACCEPTMESSAGE']._serialized_start=2151
_globals['_ADSBIDINTENTACCEPTMESSAGE']._serialized_end=2263
# @@protoc_insertion_point(module_scope)

View File

@@ -24,7 +24,6 @@ import queue
import random
import select
import socket
import struct
import hashlib
import logging
import secrets
@@ -41,7 +40,7 @@ from basicswap.contrib.rfc6979 import (
START_TOKEN = 0xabcd
MSG_START_TOKEN = struct.pack('>H', START_TOKEN)
MSG_START_TOKEN = START_TOKEN.to_bytes(2, 'big')
MSG_MAX_SIZE = 0x200000 # 2MB
@@ -83,8 +82,8 @@ class MsgHandshake:
pass
def encode_aad(self): # Additional Authenticated Data
return struct.pack('>H', NetMessageTypes.HANDSHAKE) + \
struct.pack('>Q', self._timestamp) + \
return int(NetMessageTypes.HANDSHAKE).to_bytes(2, 'big') + \
self._timestamp.to_bytes(8, 'big') + \
self._ephem_pk
def encode(self):
@@ -92,7 +91,7 @@ class MsgHandshake:
def decode(self, msg_mv):
o = 2
self._timestamp = struct.unpack('>Q', msg_mv[o: o + 8])[0]
self._timestamp = int.from_bytes(msg_mv[o: o + 8], 'big')
o += 8
self._ephem_pk = bytes(msg_mv[o: o + 33])
o += 33
@@ -333,7 +332,7 @@ class Network:
ss = k.ecdh(peer._pubkey)
hashed = hashlib.sha512(ss + struct.pack('>Q', msg._timestamp)).digest()
hashed = hashlib.sha512(ss + msg._timestamp.to_bytes(8, 'big')).digest()
peer._ke = hashed[:32]
peer._km = hashed[32:]
@@ -386,7 +385,7 @@ class Network:
nk = PrivateKey(self._network_key)
ss = nk.ecdh(msg._ephem_pk)
hashed = hashlib.sha512(ss + struct.pack('>Q', msg._timestamp)).digest()
hashed = hashlib.sha512(ss + msg._timestamp.to_bytes(8, 'big')).digest()
peer._ke = hashed[:32]
peer._km = hashed[32:]
@@ -427,7 +426,7 @@ class Network:
mac = msg_mv[-16:]
plaintext = cipher.decrypt_and_verify(msg_mv[2: -16], mac)
ping_nonce = struct.unpack('>I', plaintext[:4])[0]
ping_nonce = int.from_bytes(plaintext[:4], 'big')
# Version is added to a ping following a handshake message
if len(plaintext) >= 10:
peer._ready = True
@@ -450,7 +449,7 @@ class Network:
mac = msg_mv[-16:]
plaintext = cipher.decrypt_and_verify(msg_mv[2: -16], mac)
pong_nonce = struct.unpack('>I', plaintext[:4])[0]
pong_nonce = int.from_bytes(plaintext[:4], 'big')
if pong_nonce == peer._ping_nonce:
peer._last_ping_rtt = (time.time_ns() // 1000) - peer._last_ping_at
@@ -462,14 +461,14 @@ class Network:
def send_ping(self, peer):
ping_nonce = random.getrandbits(32)
msg_bytes = struct.pack('>H', NetMessageTypes.PING)
msg_bytes = int(NetMessageTypes.PING).to_bytes(2, 'big')
nonce = peer._sent_nonce[:24]
cipher = ChaCha20_Poly1305.new(key=peer._ke, nonce=nonce)
cipher.update(msg_bytes)
cipher.update(nonce)
payload = struct.pack('>I', ping_nonce)
payload = ping_nonce.to_bytes(4, 'big')
if peer._last_ping_at == 0:
payload += self._sc._version
ct, mac = cipher.encrypt_and_digest(payload)
@@ -484,14 +483,14 @@ class Network:
self.send_msg(peer, msg_bytes)
def send_pong(self, peer, ping_nonce):
msg_bytes = struct.pack('>H', NetMessageTypes.PONG)
msg_bytes = int(NetMessageTypes.PONG).to_bytes(2, 'big')
nonce = peer._sent_nonce[:24]
cipher = ChaCha20_Poly1305.new(key=peer._ke, nonce=nonce)
cipher.update(msg_bytes)
cipher.update(nonce)
payload = struct.pack('>I', ping_nonce)
payload = ping_nonce.to_bytes(4, 'big')
ct, mac = cipher.encrypt_and_digest(payload)
msg_bytes += ct + mac
@@ -503,7 +502,7 @@ class Network:
msg_encoded = msg if isinstance(msg, bytes) else msg.encode()
len_encoded = len(msg_encoded)
msg_packed = bytearray(MSG_START_TOKEN) + struct.pack('>I', len_encoded) + msg_encoded
msg_packed = bytearray(MSG_START_TOKEN) + len_encoded.to_bytes(4, 'big') + msg_encoded
peer._socket.sendall(msg_packed)
peer._bytes_sent += len_encoded
@@ -515,7 +514,7 @@ class Network:
try:
mv = memoryview(msg_bytes)
o = 0
msg_type = struct.unpack('>H', mv[o: o + 2])[0]
msg_type = int.from_bytes(mv[o: o + 2], 'big')
if msg_type == NetMessageTypes.HANDSHAKE:
self.process_handshake(peer, mv)
elif msg_type == NetMessageTypes.PING:
@@ -548,13 +547,13 @@ class Network:
raise ValueError('Invalid start token')
o += 2
msg_len = struct.unpack('>I', mv[o: o + 4])[0]
msg_len = int.from_bytes(mv[o: o + 4], 'big')
o += 4
if msg_len < 2 or msg_len > MSG_MAX_SIZE:
raise ValueError('Invalid data length')
# Precheck msg_type
msg_type = struct.unpack('>H', mv[o: o + 2])[0]
msg_type = int.from_bytes(mv[o: o + 2], 'big')
# o += 2 # Don't inc offset, msg includes type
if not NetMessageTypes.has_value(msg_type):
raise ValueError('Invalid msg type')

View File

@@ -23,10 +23,7 @@ from .util import jsonDecimal
def waitForRPC(rpc_func, expect_wallet=True, max_tries=7):
for i in range(max_tries + 1):
try:
if expect_wallet:
rpc_func('getwalletinfo')
else:
rpc_func('getblockchaininfo')
rpc_func('getwalletinfo' if expect_wallet else 'getblockchaininfo')
return
except Exception as ex:
if i < max_tries:
@@ -111,7 +108,7 @@ def callrpc(rpc_port, auth, method, params=[], wallet=None, host='127.0.0.1'):
r = json.loads(v.decode('utf-8'))
except Exception as ex:
traceback.print_exc()
raise ValueError('RPC server error ' + str(ex))
raise ValueError('RPC server error ' + str(ex) + ', method: ' + method)
if 'error' in r and r['error'] is not None:
raise ValueError('RPC error ' + str(r['error']))

View File

@@ -2,6 +2,7 @@
import os
import json
import socks
import time
import urllib
import hashlib
@@ -10,9 +11,32 @@ from xmlrpc.client import (
Transport,
SafeTransport,
)
from sockshandler import SocksiPyConnection
from .util import jsonDecimal
class SocksTransport(Transport):
def set_proxy(self, proxy_host, proxy_port):
self.proxy_host = proxy_host
self.proxy_port = proxy_port
self.proxy_type = socks.PROXY_TYPE_SOCKS5
self.proxy_rdns = True
self.proxy_username = None
self.proxy_password = None
def make_connection(self, host):
# return an existing connection if possible. This allows
# HTTP/1.1 keep-alive.
if self._connection and host == self._connection[0]:
return self._connection[1]
# create a HTTP connection object from a host descriptor
chost, self._extra_headers, x509 = self.get_host_info(host)
self._connection = host, SocksiPyConnection(self.proxy_type, self.proxy_host, self.proxy_port, self.proxy_rdns, self.proxy_username, self.proxy_password, chost)
return self._connection[1]
class JsonrpcDigest():
# __getattr__ complicates extending ServerProxy
def __init__(self, uri, transport=None, encoding=None, verbose=False,
@@ -148,7 +172,7 @@ class JsonrpcDigest():
raise
def callrpc_xmr(rpc_port, method, params=[], rpc_host='127.0.0.1', path='json_rpc', auth=None, timeout=120):
def callrpc_xmr(rpc_port, method, params=[], rpc_host='127.0.0.1', path='json_rpc', auth=None, timeout=120, transport=None, tag=''):
# auth is a tuple: (username, password)
try:
if rpc_host.count('://') > 0:
@@ -156,7 +180,7 @@ def callrpc_xmr(rpc_port, method, params=[], rpc_host='127.0.0.1', path='json_rp
else:
url = 'http://{}:{}/{}'.format(rpc_host, rpc_port, path)
x = JsonrpcDigest(url)
x = JsonrpcDigest(url, transport=transport)
request_body = {
'method': method,
'params': params,
@@ -170,22 +194,22 @@ def callrpc_xmr(rpc_port, method, params=[], rpc_host='127.0.0.1', path='json_rp
x.close()
r = json.loads(v.decode('utf-8'))
except Exception as ex:
raise ValueError('RPC Server Error: {}'.format(str(ex)))
raise ValueError('{}RPC Server Error: {}'.format(tag, str(ex)))
if 'error' in r and r['error'] is not None:
raise ValueError('RPC error ' + str(r['error']))
raise ValueError(tag + 'RPC error ' + str(r['error']))
return r['result']
def callrpc_xmr2(rpc_port: int, method: str, params=None, auth=None, rpc_host='127.0.0.1', timeout=120):
def callrpc_xmr2(rpc_port: int, method: str, params=None, auth=None, rpc_host='127.0.0.1', timeout=120, transport=None, tag=''):
try:
if rpc_host.count('://') > 0:
url = '{}:{}/{}'.format(rpc_host, rpc_port, method)
else:
url = 'http://{}:{}/{}'.format(rpc_host, rpc_port, method)
x = JsonrpcDigest(url)
x = JsonrpcDigest(url, transport=transport)
if auth:
v = x.json_request(params, username=auth[0], password=auth[1], timeout=timeout)
else:
@@ -193,28 +217,42 @@ def callrpc_xmr2(rpc_port: int, method: str, params=None, auth=None, rpc_host='1
x.close()
r = json.loads(v.decode('utf-8'))
except Exception as ex:
raise ValueError('RPC Server Error: {}'.format(str(ex)))
raise ValueError('{}RPC Server Error: {}'.format(tag, str(ex)))
return r
def make_xmr_rpc2_func(port, auth, host='127.0.0.1'):
def make_xmr_rpc2_func(port, auth, host='127.0.0.1', proxy_host=None, proxy_port=None, default_timeout=120, tag=''):
port = port
auth = auth
host = host
transport = None
default_timeout = default_timeout
tag = tag
def rpc_func(method, params=None, wallet=None, timeout=120):
nonlocal port, auth, host
return callrpc_xmr2(port, method, params, auth=auth, rpc_host=host, timeout=timeout)
if proxy_host:
transport = SocksTransport()
transport.set_proxy(proxy_host, proxy_port)
def rpc_func(method, params=None, wallet=None, timeout=default_timeout):
nonlocal port, auth, host, transport, tag
return callrpc_xmr2(port, method, params, auth=auth, rpc_host=host, timeout=timeout, transport=transport, tag=tag)
return rpc_func
def make_xmr_rpc_func(port, auth, host='127.0.0.1'):
def make_xmr_rpc_func(port, auth, host='127.0.0.1', proxy_host=None, proxy_port=None, default_timeout=120, tag=''):
port = port
auth = auth
host = host
transport = None
default_timeout = default_timeout
tag = tag
def rpc_func(method, params=None, wallet=None, timeout=120):
nonlocal port, auth, host
return callrpc_xmr(port, method, params, rpc_host=host, auth=auth, timeout=timeout)
if proxy_host:
transport = SocksTransport()
transport.set_proxy(proxy_host, proxy_port)
def rpc_func(method, params=None, wallet=None, timeout=default_timeout):
nonlocal port, auth, host, transport, tag
return callrpc_xmr(port, method, params, rpc_host=host, auth=auth, timeout=timeout, transport=transport, tag=tag)
return rpc_func

View File

@@ -96,6 +96,17 @@
display: block;
}
.blurred {
filter: blur(4px);
pointer-events: none;
user-select: none;
}
.error-overlay.non-blurred {
filter: none;
pointer-events: auto;
user-select: auto;
}
/* Disable opacity on disabled form elements in Chrome */
@media screen and (-webkit-min-device-pixel-ratio:0) {

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

View File

@@ -1,4 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" id="mscgenjsreplaceme" width="1272" height="2063.25" class="mscgenjsreplaceme" style="font-family:Helvetica,sans-serif;font-size:12px;font-weight:400;font-style:normal;text-decoration:none;background-color:#fff;stroke:#000;stroke-width:2" version="1.1">
<svg xmlns="http://www.w3.org/2000/svg" id="mscgenjsreplaceme" width="1272" height="2101.25" class="mscgenjsreplaceme" style="font-family:Helvetica,sans-serif;font-size:12px;font-weight:400;font-style:normal;text-decoration:none;background-color:#fff;stroke:#000;stroke-width:2" version="1.1">
<defs>
<marker id="mscgenjsreplacemecallback-#0000FF" class="arrow-marker" markerHeight="10" markerUnits="strokeWidth" markerWidth="10" orient="auto" refX="9" refY="3" viewBox="0 0 10 10">
<path d="m1 1 8 2-8 2" class="arrow-style" style="stroke-dasharray:100,1;stroke:#00f"/>
@@ -35,10 +35,10 @@
</style>
</defs>
<g id="mscgenjsreplaceme_body" transform="translate(51 3)">
<path id="mscgenjsreplaceme_background" d="M-51-3h1272v2063.25H-51z" class="bglayer" style="fill:#fff;stroke:#fff;stroke-width:0"/>
<path id="mscgenjsreplaceme_background" d="M-51-3h1272v2101.25H-51z" class="bglayer" style="fill:#fff;stroke:#fff;stroke-width:0"/>
<g id="mscgenjsreplaceme_arcspans">
<path d="M-41 869.1h1044v1169.15H-41z" class="box inline_expression alt"/>
<path d="M-37 1410.15H999v590.1H-37z" class="box inline_expression alt"/>
<path d="M-41 907.1h1044v1169.15H-41z" class="box inline_expression alt"/>
<path d="M-37 1448.15H999v590.1H-37z" class="box inline_expression alt"/>
</g>
<g id="mscgenjsreplaceme_lifelines">
<path d="M65 38v38" class="arcrow" style="stroke:transparent"/>
@@ -97,39 +97,39 @@
<path d="M273 548v38" class="arcrow" style="stroke:#080"/>
<path d="M481 548v38" class="arcrow" style="stroke:red"/>
<path d="M689 548v38" class="arcrow" style="stroke:#00f"/>
<path d="M897 548v38M1105 548v38M65 586v75.05" class="arcrow" style="stroke:transparent"/>
<path d="M273 586v75.05" class="arcrow" style="stroke:#080"/>
<path d="M481 586v75.05" class="arcrow" style="stroke:red"/>
<path d="M689 586v75.05" class="arcrow" style="stroke:#00f"/>
<path d="M897 586v75.05M1105 586v75.05M65 661.05v38" class="arcrow" style="stroke:transparent"/>
<path d="M273 661.05v38" class="arcrow" style="stroke:#080"/>
<path d="M481 661.05v38" class="arcrow" style="stroke:red"/>
<path d="M689 661.05v38" class="arcrow" style="stroke:#00f"/>
<path d="M897 661.05v38M1105 661.05v38M65 699.05v38" class="arcrow" style="stroke:transparent"/>
<path d="M897 548v38M1105 548v38M65 586v38" class="arcrow" style="stroke:transparent"/>
<path d="M273 586v38" class="arcrow" style="stroke:#080"/>
<path d="M481 586v38" class="arcrow" style="stroke:red"/>
<path d="M689 586v38" class="arcrow" style="stroke:#00f"/>
<path d="M897 586v38M1105 586v38M65 624v75.05" class="arcrow" style="stroke:transparent"/>
<path d="M273 624v75.05" class="arcrow" style="stroke:#080"/>
<path d="M481 624v75.05" class="arcrow" style="stroke:red"/>
<path d="M689 624v75.05" class="arcrow" style="stroke:#00f"/>
<path d="M897 624v75.05M1105 624v75.05M65 699.05v38" class="arcrow" style="stroke:transparent"/>
<path d="M273 699.05v38" class="arcrow" style="stroke:#080"/>
<path d="M481 699.05v38" class="arcrow" style="stroke:red"/>
<path d="M689 699.05v38" class="arcrow" style="stroke:#00f"/>
<path d="M897 699.05v38M1105 699.05v38M65 737.05v75.05" class="arcrow" style="stroke:transparent"/>
<path d="M273 737.05v75.05" class="arcrow" style="stroke:#080"/>
<path d="M481 737.05v75.05" class="arcrow" style="stroke:red"/>
<path d="M689 737.05v75.05" class="arcrow" style="stroke:#00f"/>
<path d="M897 737.05v75.05M1105 737.05v75.05M65 812.1v38" class="arcrow" style="stroke:transparent"/>
<path d="M273 812.1v38" class="arcrow" style="stroke:#080"/>
<path d="M481 812.1v38" class="arcrow" style="stroke:red"/>
<path d="M689 812.1v38" class="arcrow" style="stroke:#00f"/>
<path d="M897 812.1v38M1105 812.1v38M65 850.1v38" class="arcrow" style="stroke:transparent"/>
<path d="M897 699.05v38M1105 699.05v38M65 737.05v38" class="arcrow" style="stroke:transparent"/>
<path d="M273 737.05v38" class="arcrow" style="stroke:#080"/>
<path d="M481 737.05v38" class="arcrow" style="stroke:red"/>
<path d="M689 737.05v38" class="arcrow" style="stroke:#00f"/>
<path d="M897 737.05v38M1105 737.05v38M65 775.05v75.05" class="arcrow" style="stroke:transparent"/>
<path d="M273 775.05v75.05" class="arcrow" style="stroke:#080"/>
<path d="M481 775.05v75.05" class="arcrow" style="stroke:red"/>
<path d="M689 775.05v75.05" class="arcrow" style="stroke:#00f"/>
<path d="M897 775.05v75.05M1105 775.05v75.05M65 850.1v38" class="arcrow" style="stroke:transparent"/>
<path d="M273 850.1v38" class="arcrow" style="stroke:#080"/>
<path d="M481 850.1v38" class="arcrow" style="stroke:red"/>
<path d="M689 850.1v38" class="arcrow" style="stroke:#00f"/>
<path d="M897 850.1v38M1105 850.1v38M65 888.1v86" class="arcrow" style="stroke:transparent"/>
<path d="M273 888.1v86" class="arcrow" style="stroke:#080"/>
<path d="M481 888.1v86" class="arcrow" style="stroke:red"/>
<path d="M689 888.1v86" class="arcrow" style="stroke:#00f"/>
<path d="M897 888.1v86M1105 888.1v86M65 974.1v38" class="arcrow" style="stroke:transparent"/>
<path d="M273 974.1v38" class="arcrow" style="stroke:#080"/>
<path d="M481 974.1v38" class="arcrow" style="stroke:red"/>
<path d="M689 974.1v38" class="arcrow" style="stroke:#00f"/>
<path d="M897 974.1v38M1105 974.1v38M65 1012.1v38" class="arcrow" style="stroke:transparent"/>
<path d="M897 850.1v38M1105 850.1v38M65 888.1v38" class="arcrow" style="stroke:transparent"/>
<path d="M273 888.1v38" class="arcrow" style="stroke:#080"/>
<path d="M481 888.1v38" class="arcrow" style="stroke:red"/>
<path d="M689 888.1v38" class="arcrow" style="stroke:#00f"/>
<path d="M897 888.1v38M1105 888.1v38M65 926.1v86" class="arcrow" style="stroke:transparent"/>
<path d="M273 926.1v86" class="arcrow" style="stroke:#080"/>
<path d="M481 926.1v86" class="arcrow" style="stroke:red"/>
<path d="M689 926.1v86" class="arcrow" style="stroke:#00f"/>
<path d="M897 926.1v86M1105 926.1v86M65 1012.1v38" class="arcrow" style="stroke:transparent"/>
<path d="M273 1012.1v38" class="arcrow" style="stroke:#080"/>
<path d="M481 1012.1v38" class="arcrow" style="stroke:red"/>
<path d="M689 1012.1v38" class="arcrow" style="stroke:#00f"/>
@@ -149,15 +149,15 @@
<path d="M273 1164.1v38" class="arcrow" style="stroke:#080"/>
<path d="M481 1164.1v38" class="arcrow" style="stroke:red"/>
<path d="M689 1164.1v38" class="arcrow" style="stroke:#00f"/>
<path d="M897 1164.1v38M1105 1164.1v38M65 1202.1v75.05" class="arcrow" style="stroke:transparent"/>
<path d="M273 1202.1v75.05" class="arcrow" style="stroke:#080"/>
<path d="M481 1202.1v75.05" class="arcrow" style="stroke:red"/>
<path d="M689 1202.1v75.05" class="arcrow" style="stroke:#00f"/>
<path d="M897 1202.1v75.05M1105 1202.1v75.05M65 1277.15v38" class="arcrow" style="stroke:transparent"/>
<path d="M273 1277.15v38" class="arcrow" style="stroke:#080"/>
<path d="M481 1277.15v38" class="arcrow" style="stroke:red"/>
<path d="M689 1277.15v38" class="arcrow" style="stroke:#00f"/>
<path d="M897 1277.15v38M1105 1277.15v38M65 1315.15v38" class="arcrow" style="stroke:transparent"/>
<path d="M897 1164.1v38M1105 1164.1v38M65 1202.1v38" class="arcrow" style="stroke:transparent"/>
<path d="M273 1202.1v38" class="arcrow" style="stroke:#080"/>
<path d="M481 1202.1v38" class="arcrow" style="stroke:red"/>
<path d="M689 1202.1v38" class="arcrow" style="stroke:#00f"/>
<path d="M897 1202.1v38M1105 1202.1v38M65 1240.1v75.05" class="arcrow" style="stroke:transparent"/>
<path d="M273 1240.1v75.05" class="arcrow" style="stroke:#080"/>
<path d="M481 1240.1v75.05" class="arcrow" style="stroke:red"/>
<path d="M689 1240.1v75.05" class="arcrow" style="stroke:#00f"/>
<path d="M897 1240.1v75.05M1105 1240.1v75.05M65 1315.15v38" class="arcrow" style="stroke:transparent"/>
<path d="M273 1315.15v38" class="arcrow" style="stroke:#080"/>
<path d="M481 1315.15v38" class="arcrow" style="stroke:red"/>
<path d="M689 1315.15v38" class="arcrow" style="stroke:#00f"/>
@@ -173,19 +173,19 @@
<path d="M273 1429.15v38" class="arcrow" style="stroke:#080"/>
<path d="M481 1429.15v38" class="arcrow" style="stroke:red"/>
<path d="M689 1429.15v38" class="arcrow" style="stroke:#00f"/>
<path d="M897 1429.15v38M1105 1429.15v38M65 1467.15v59.05" class="arcrow" style="stroke:transparent"/>
<path d="M273 1467.15v59.05" class="arcrow" style="stroke:#080"/>
<path d="M481 1467.15v59.05" class="arcrow" style="stroke:red"/>
<path d="M689 1467.15v59.05" class="arcrow" style="stroke:#00f"/>
<path d="M897 1467.15v59.05M1105 1467.15v59.05M65 1526.2v54" class="arcrow" style="stroke:transparent"/>
<path d="M273 1526.2v54" class="arcrow" style="stroke:#080"/>
<path d="M481 1526.2v54" class="arcrow" style="stroke:red"/>
<path d="M689 1526.2v54" class="arcrow" style="stroke:#00f"/>
<path d="M897 1526.2v54M1105 1526.2v54M65 1580.2v38" class="arcrow" style="stroke:transparent"/>
<path d="M273 1580.2v38" class="arcrow" style="stroke:#080"/>
<path d="M481 1580.2v38" class="arcrow" style="stroke:red"/>
<path d="M689 1580.2v38" class="arcrow" style="stroke:#00f"/>
<path d="M897 1580.2v38M1105 1580.2v38M65 1618.2v38" class="arcrow" style="stroke:transparent"/>
<path d="M897 1429.15v38M1105 1429.15v38M65 1467.15v38" class="arcrow" style="stroke:transparent"/>
<path d="M273 1467.15v38" class="arcrow" style="stroke:#080"/>
<path d="M481 1467.15v38" class="arcrow" style="stroke:red"/>
<path d="M689 1467.15v38" class="arcrow" style="stroke:#00f"/>
<path d="M897 1467.15v38M1105 1467.15v38M65 1505.15v59.05" class="arcrow" style="stroke:transparent"/>
<path d="M273 1505.15v59.05" class="arcrow" style="stroke:#080"/>
<path d="M481 1505.15v59.05" class="arcrow" style="stroke:red"/>
<path d="M689 1505.15v59.05" class="arcrow" style="stroke:#00f"/>
<path d="M897 1505.15v59.05M1105 1505.15v59.05M65 1564.2v54" class="arcrow" style="stroke:transparent"/>
<path d="M273 1564.2v54" class="arcrow" style="stroke:#080"/>
<path d="M481 1564.2v54" class="arcrow" style="stroke:red"/>
<path d="M689 1564.2v54" class="arcrow" style="stroke:#00f"/>
<path d="M897 1564.2v54M1105 1564.2v54M65 1618.2v38" class="arcrow" style="stroke:transparent"/>
<path d="M273 1618.2v38" class="arcrow" style="stroke:#080"/>
<path d="M481 1618.2v38" class="arcrow" style="stroke:red"/>
<path d="M689 1618.2v38" class="arcrow" style="stroke:#00f"/>
@@ -209,15 +209,15 @@
<path d="M273 1808.2v38" class="arcrow" style="stroke:#080"/>
<path d="M481 1808.2v38" class="arcrow" style="stroke:red"/>
<path d="M689 1808.2v38" class="arcrow" style="stroke:#00f"/>
<path d="M897 1808.2v38M1105 1808.2v38M65 1846.2v59.05" class="arcrow" style="stroke:transparent"/>
<path d="M273 1846.2v59.05" class="arcrow" style="stroke:#080"/>
<path d="M481 1846.2v59.05" class="arcrow" style="stroke:red"/>
<path d="M689 1846.2v59.05" class="arcrow" style="stroke:#00f"/>
<path d="M897 1846.2v59.05M1105 1846.2v59.05M65 1905.25v38" class="arcrow" style="stroke:transparent"/>
<path d="M273 1905.25v38" class="arcrow" style="stroke:#080"/>
<path d="M481 1905.25v38" class="arcrow" style="stroke:red"/>
<path d="M689 1905.25v38" class="arcrow" style="stroke:#00f"/>
<path d="M897 1905.25v38M1105 1905.25v38M65 1943.25v38" class="arcrow" style="stroke:transparent"/>
<path d="M897 1808.2v38M1105 1808.2v38M65 1846.2v38" class="arcrow" style="stroke:transparent"/>
<path d="M273 1846.2v38" class="arcrow" style="stroke:#080"/>
<path d="M481 1846.2v38" class="arcrow" style="stroke:red"/>
<path d="M689 1846.2v38" class="arcrow" style="stroke:#00f"/>
<path d="M897 1846.2v38M1105 1846.2v38M65 1884.2v59.05" class="arcrow" style="stroke:transparent"/>
<path d="M273 1884.2v59.05" class="arcrow" style="stroke:#080"/>
<path d="M481 1884.2v59.05" class="arcrow" style="stroke:red"/>
<path d="M689 1884.2v59.05" class="arcrow" style="stroke:#00f"/>
<path d="M897 1884.2v59.05M1105 1884.2v59.05M65 1943.25v38" class="arcrow" style="stroke:transparent"/>
<path d="M273 1943.25v38" class="arcrow" style="stroke:#080"/>
<path d="M481 1943.25v38" class="arcrow" style="stroke:red"/>
<path d="M689 1943.25v38" class="arcrow" style="stroke:#00f"/>
@@ -229,7 +229,11 @@
<path d="M273 2019.25v38" class="arcrow" style="stroke:#080"/>
<path d="M481 2019.25v38" class="arcrow" style="stroke:red"/>
<path d="M689 2019.25v38" class="arcrow" style="stroke:#00f"/>
<path d="M897 2019.25v38M1105 2019.25v38" class="arcrow" style="stroke:transparent"/>
<path d="M897 2019.25v38M1105 2019.25v38M65 2057.25v38" class="arcrow" style="stroke:transparent"/>
<path d="M273 2057.25v38" class="arcrow" style="stroke:#080"/>
<path d="M481 2057.25v38" class="arcrow" style="stroke:red"/>
<path d="M689 2057.25v38" class="arcrow" style="stroke:#00f"/>
<path d="M897 2057.25v38M1105 2057.25v38" class="arcrow" style="stroke:transparent"/>
</g>
<g id="mscgenjsreplaceme_sequence">
<path d="M0 0h130v38H0z" class="entity" style="stroke:transparent"/>
@@ -268,85 +272,85 @@
<path marker-end="url(#mscgenjsreplacemecallback-#0000FF)" d="M689 529H273" class="arc directional callback" style="stroke:#00f"/>
<path d="M415.64 513.25h130.72v14H415.64z" class="label-text-background"/>
<text x="481" y="524.25" class="directional-text callback-text"><tspan>Sends script-coin-lock-tx</tspan></text>
<path marker-end="url(#mscgenjsreplacememethod-#FF0000)" d="M481 615.92c104 .1 104 22.8 0 22.8" class="arc directional method" style="stroke:red"/>
<path d="M484 567.67h40.91v14H484z" class="label-text-background"/>
<text x="484" y="578.67" class="directional-text method-text anchor-start"><tspan>Wait for</tspan></text>
<path d="M484 583.67h107.01v14H484z" class="label-text-background"/>
<text x="484" y="594.67" class="directional-text method-text anchor-start"><tspan>script-coin-lock-tx to</tspan></text>
<path d="M484 599.67h39.34v14H484z" class="label-text-background"/>
<text x="484" y="610.67" class="directional-text method-text anchor-start"><tspan>confirm</tspan></text>
<path marker-end="url(#mscgenjsreplacemecallback-#FF0000)" d="M481 718.05H273" class="arc directional callback" style="stroke:red"/>
<path d="M304.97 702.3h144.06v14H304.97z" class="label-text-background"/>
<text x="377" y="713.3" class="directional-text callback-text"><tspan>Sends noscript-coin-lock-tx</tspan></text>
<path marker-end="url(#mscgenjsreplacememethod-#FF0000)" d="M481 766.97c104 .1 104 22.8 0 22.8" class="arc directional method" style="stroke:red"/>
<path d="M484 718.72h40.91v14H484z" class="label-text-background"/>
<text x="484" y="729.72" class="directional-text method-text anchor-start"><tspan>Wait for</tspan></text>
<path d="M484 734.72h120.36v14H484z" class="label-text-background"/>
<text x="484" y="745.72" class="directional-text method-text anchor-start"><tspan>noscript-coin-lock-tx to</tspan></text>
<path d="M484 750.72h39.34v14H484z" class="label-text-background"/>
<text x="484" y="761.72" class="directional-text method-text anchor-start"><tspan>confirm</tspan></text>
<path marker-end="url(#mscgenjsreplacememethod-#0000FF)" d="M689 766.97c104 .1 104 22.8 0 22.8" class="arc directional method" style="stroke:#00f"/>
<path d="M692 718.72h40.91v14H692z" class="label-text-background"/>
<text x="692" y="729.72" class="directional-text method-text anchor-start"><tspan>Wait for</tspan></text>
<path d="M692 734.72h120.36v14H692z" class="label-text-background"/>
<text x="692" y="745.72" class="directional-text method-text anchor-start"><tspan>noscript-coin-lock-tx to</tspan></text>
<path d="M692 750.72h39.34v14H692z" class="label-text-background"/>
<text x="692" y="761.72" class="directional-text method-text anchor-start"><tspan>confirm</tspan></text>
<path marker-end="url(#mscgenjsreplacememethod-#0000FF)" d="M689 931.1H481" class="arc directional method" style="stroke:#00f"/>
<path d="M519.64 915.35h130.72v14H519.64z" class="label-text-background"/>
<text x="585" y="926.35" class="directional-text method-text"><tspan>Sends script-coin-lock-tx</tspan></text>
<path d="M539.3 933.35h91.71v14H539.3z" class="label-text-background"/>
<text x="585" y="944.35" class="directional-text method-text"><tspan>release message</tspan></text>
<path marker-end="url(#mscgenjsreplacemecallback-#FF0000)" d="M481 1031.1H273" class="arc directional callback" style="stroke:red"/>
<path d="M293.3 1015.35h167.41v14H293.3z" class="label-text-background"/>
<text x="377" y="1026.35" class="directional-text callback-text"><tspan>Sends script-coin-lock-spend-tx</tspan></text>
<path d="M-41 1145.1h1044" class="inline_expression_divider"/>
<path d="M459.98 1137.85h42.03v14h-42.03z" class="label-text-background"/>
<text x="481" y="1148.85" class="empty-text comment-row-text"><tspan>fail path</tspan></text>
<path marker-end="url(#mscgenjsreplacememethod-#FF0000)" d="M481 1232.02c104 .1 104 22.8 0 22.8" class="arc directional method" style="stroke:red"/>
<path d="M484 1183.77h40.91v14H484z" class="label-text-background"/>
<text x="484" y="1194.77" class="directional-text method-text anchor-start"><tspan>Wait for</tspan></text>
<path d="M484 1199.77h131.69v14H484z" class="label-text-background"/>
<text x="484" y="1210.77" class="directional-text method-text anchor-start"><tspan>script-coin-lock-tx lock to</tspan></text>
<path d="M484 1215.77h33.01v14H484z" class="label-text-background"/>
<text x="484" y="1226.77" class="directional-text method-text anchor-start"><tspan>expire</tspan></text>
<path marker-end="url(#mscgenjsreplacemecallback-#0000FF)" d="M689 1296.15H273" class="arc directional callback" style="stroke:#00f"/>
<path d="M385.96 1280.4h190.08v14H385.96z" class="label-text-background"/>
<text x="481" y="1291.4" class="directional-text callback-text"><tspan>Sends script-coin-lock-pre-refund-tx</tspan></text>
<path marker-end="url(#mscgenjsreplacemecallback-#008800)" d="M273 1334.15h416" class="arc directional return" style="stroke:#080"/>
<path d="M404.64 1318.4h152.72v14H404.64z" class="label-text-background"/>
<text x="481" y="1329.4" class="directional-text return-text"><tspan>script-coin-lock-pre-refund-tx</tspan></text>
<path marker-end="url(#mscgenjsreplacememethod-#0000FF)" d="M689 1489.07c104 .1 104 22.8 0 22.8" class="arc directional method" style="stroke:#00f"/>
<path d="M692 1456.82h40.91v14H692z" class="label-text-background"/>
<text x="692" y="1467.82" class="directional-text method-text anchor-start"><tspan>Wait for</tspan></text>
<path d="M692 1472.82h124.06v14H692z" class="label-text-background"/>
<text x="692" y="1483.82" class="directional-text method-text anchor-start"><tspan>pre-refund tx to confirm</tspan></text>
<path marker-end="url(#mscgenjsreplacemecallback-#0000FF)" d="M689 1553.2H273" class="arc directional callback" style="stroke:#00f"/>
<path d="M367.62 1537.45h226.77v14H367.62z" class="label-text-background"/>
<text x="481" y="1548.45" class="directional-text callback-text"><tspan>Sends script-coin-lock-pre-refund-spend-tx</tspan></text>
<path marker-end="url(#mscgenjsreplacemecallback-#008800)" d="M273 1637.2h208" class="arc directional return" style="stroke:#080"/>
<path d="M356.66 1621.45h40.69v14h-40.69z" class="label-text-background"/>
<text x="377" y="1632.45" class="directional-text return-text"><tspan>Detects</tspan></text>
<path d="M282.3 1639.45h189.41v14H282.3z" class="label-text-background"/>
<text x="377" y="1650.45" class="directional-text return-text"><tspan>script-coin-lock-pre-refund-spend-tx</tspan></text>
<path marker-end="url(#mscgenjsreplacemecallback-#FF0000)" d="M481 1675.2H273" class="arc directional callback" style="stroke:red"/>
<path d="M359.98 1659.45h34.03v14h-34.03z" class="label-text-background"/>
<text x="377" y="1670.45" class="directional-text callback-text"><tspan>Sends</tspan></text>
<path d="M297.65 1677.45h158.7v14h-158.7z" class="label-text-background"/>
<text x="377" y="1688.45" class="directional-text callback-text"><tspan>scriptless-coin-lock-recover-tx</tspan></text>
<path d="M-37 1789.2H999" class="inline_expression_divider"/>
<path d="M396.41 1781.95h169.17v14H396.41z" class="label-text-background"/>
<text x="481" y="1792.95" class="empty-text comment-row-text"><tspan>offerer swipes script coin lock tx</tspan></text>
<path marker-end="url(#mscgenjsreplacememethod-#FF0000)" d="M481 1868.12c104 .1 104 22.8 0 22.8" class="arc directional method" style="stroke:red"/>
<path d="M484 1835.87h40.91v14H484z" class="label-text-background"/>
<text x="484" y="1846.87" class="directional-text method-text anchor-start"><tspan>Wait for</tspan></text>
<path d="M484 1851.87h142.39v14H484z" class="label-text-background"/>
<text x="484" y="1862.87" class="directional-text method-text anchor-start"><tspan>pre-refund tx lock to expire</tspan></text>
<path marker-end="url(#mscgenjsreplacemecallback-#FF0000)" d="M481 1924.25H273" class="arc directional callback" style="stroke:red"/>
<path d="M359.98 1908.5h34.03v14h-34.03z" class="label-text-background"/>
<text x="377" y="1919.5" class="directional-text callback-text"><tspan>Sends</tspan></text>
<path d="M283.3 1926.5h187.39v14H283.3z" class="label-text-background"/>
<text x="377" y="1937.5" class="directional-text callback-text"><tspan>script-coin-lock-pre-refund-swipe-tx</tspan></text>
<path marker-end="url(#mscgenjsreplacememethod-#FF0000)" d="M481 653.92c104 .1 104 22.8 0 22.8" class="arc directional method" style="stroke:red"/>
<path d="M484 605.67h40.91v14H484z" class="label-text-background"/>
<text x="484" y="616.67" class="directional-text method-text anchor-start"><tspan>Wait for</tspan></text>
<path d="M484 621.67h107.01v14H484z" class="label-text-background"/>
<text x="484" y="632.67" class="directional-text method-text anchor-start"><tspan>script-coin-lock-tx to</tspan></text>
<path d="M484 637.67h39.34v14H484z" class="label-text-background"/>
<text x="484" y="648.67" class="directional-text method-text anchor-start"><tspan>confirm</tspan></text>
<path marker-end="url(#mscgenjsreplacemecallback-#FF0000)" d="M481 756.05H273" class="arc directional callback" style="stroke:red"/>
<path d="M304.97 740.3h144.06v14H304.97z" class="label-text-background"/>
<text x="377" y="751.3" class="directional-text callback-text"><tspan>Sends noscript-coin-lock-tx</tspan></text>
<path marker-end="url(#mscgenjsreplacememethod-#FF0000)" d="M481 804.97c104 .1 104 22.8 0 22.8" class="arc directional method" style="stroke:red"/>
<path d="M484 756.72h40.91v14H484z" class="label-text-background"/>
<text x="484" y="767.72" class="directional-text method-text anchor-start"><tspan>Wait for</tspan></text>
<path d="M484 772.72h120.36v14H484z" class="label-text-background"/>
<text x="484" y="783.72" class="directional-text method-text anchor-start"><tspan>noscript-coin-lock-tx to</tspan></text>
<path d="M484 788.72h39.34v14H484z" class="label-text-background"/>
<text x="484" y="799.72" class="directional-text method-text anchor-start"><tspan>confirm</tspan></text>
<path marker-end="url(#mscgenjsreplacememethod-#0000FF)" d="M689 804.97c104 .1 104 22.8 0 22.8" class="arc directional method" style="stroke:#00f"/>
<path d="M692 756.72h40.91v14H692z" class="label-text-background"/>
<text x="692" y="767.72" class="directional-text method-text anchor-start"><tspan>Wait for</tspan></text>
<path d="M692 772.72h120.36v14H692z" class="label-text-background"/>
<text x="692" y="783.72" class="directional-text method-text anchor-start"><tspan>noscript-coin-lock-tx to</tspan></text>
<path d="M692 788.72h39.34v14H692z" class="label-text-background"/>
<text x="692" y="799.72" class="directional-text method-text anchor-start"><tspan>confirm</tspan></text>
<path marker-end="url(#mscgenjsreplacememethod-#0000FF)" d="M689 969.1H481" class="arc directional method" style="stroke:#00f"/>
<path d="M519.64 953.35h130.72v14H519.64z" class="label-text-background"/>
<text x="585" y="964.35" class="directional-text method-text"><tspan>Sends script-coin-lock-tx</tspan></text>
<path d="M539.3 971.35h91.71v14H539.3z" class="label-text-background"/>
<text x="585" y="982.35" class="directional-text method-text"><tspan>release message</tspan></text>
<path marker-end="url(#mscgenjsreplacemecallback-#FF0000)" d="M481 1069.1H273" class="arc directional callback" style="stroke:red"/>
<path d="M293.3 1053.35h167.41v14H293.3z" class="label-text-background"/>
<text x="377" y="1064.35" class="directional-text callback-text"><tspan>Sends script-coin-lock-spend-tx</tspan></text>
<path d="M-41 1183.1h1044" class="inline_expression_divider"/>
<path d="M459.98 1175.85h42.03v14h-42.03z" class="label-text-background"/>
<text x="481" y="1186.85" class="empty-text comment-row-text"><tspan>fail path</tspan></text>
<path marker-end="url(#mscgenjsreplacememethod-#FF0000)" d="M481 1270.02c104 .1 104 22.8 0 22.8" class="arc directional method" style="stroke:red"/>
<path d="M484 1221.77h40.91v14H484z" class="label-text-background"/>
<text x="484" y="1232.77" class="directional-text method-text anchor-start"><tspan>Wait for</tspan></text>
<path d="M484 1237.77h131.69v14H484z" class="label-text-background"/>
<text x="484" y="1248.77" class="directional-text method-text anchor-start"><tspan>script-coin-lock-tx lock to</tspan></text>
<path d="M484 1253.77h33.01v14H484z" class="label-text-background"/>
<text x="484" y="1264.77" class="directional-text method-text anchor-start"><tspan>expire</tspan></text>
<path marker-end="url(#mscgenjsreplacemecallback-#0000FF)" d="M689 1334.15H273" class="arc directional callback" style="stroke:#00f"/>
<path d="M385.96 1318.4h190.08v14H385.96z" class="label-text-background"/>
<text x="481" y="1329.4" class="directional-text callback-text"><tspan>Sends script-coin-lock-pre-refund-tx</tspan></text>
<path marker-end="url(#mscgenjsreplacemecallback-#008800)" d="M273 1372.15h416" class="arc directional return" style="stroke:#080"/>
<path d="M404.64 1356.4h152.72v14H404.64z" class="label-text-background"/>
<text x="481" y="1367.4" class="directional-text return-text"><tspan>script-coin-lock-pre-refund-tx</tspan></text>
<path marker-end="url(#mscgenjsreplacememethod-#0000FF)" d="M689 1527.07c104 .1 104 22.8 0 22.8" class="arc directional method" style="stroke:#00f"/>
<path d="M692 1494.82h40.91v14H692z" class="label-text-background"/>
<text x="692" y="1505.82" class="directional-text method-text anchor-start"><tspan>Wait for</tspan></text>
<path d="M692 1510.82h124.06v14H692z" class="label-text-background"/>
<text x="692" y="1521.82" class="directional-text method-text anchor-start"><tspan>pre-refund tx to confirm</tspan></text>
<path marker-end="url(#mscgenjsreplacemecallback-#0000FF)" d="M689 1591.2H273" class="arc directional callback" style="stroke:#00f"/>
<path d="M367.62 1575.45h226.77v14H367.62z" class="label-text-background"/>
<text x="481" y="1586.45" class="directional-text callback-text"><tspan>Sends script-coin-lock-pre-refund-spend-tx</tspan></text>
<path marker-end="url(#mscgenjsreplacemecallback-#008800)" d="M273 1675.2h208" class="arc directional return" style="stroke:#080"/>
<path d="M356.66 1659.45h40.69v14h-40.69z" class="label-text-background"/>
<text x="377" y="1670.45" class="directional-text return-text"><tspan>Detects</tspan></text>
<path d="M282.3 1677.45h189.41v14H282.3z" class="label-text-background"/>
<text x="377" y="1688.45" class="directional-text return-text"><tspan>script-coin-lock-pre-refund-spend-tx</tspan></text>
<path marker-end="url(#mscgenjsreplacemecallback-#FF0000)" d="M481 1713.2H273" class="arc directional callback" style="stroke:red"/>
<path d="M359.98 1697.45h34.03v14h-34.03z" class="label-text-background"/>
<text x="377" y="1708.45" class="directional-text callback-text"><tspan>Sends</tspan></text>
<path d="M297.65 1715.45h158.7v14h-158.7z" class="label-text-background"/>
<text x="377" y="1726.45" class="directional-text callback-text"><tspan>scriptless-coin-lock-recover-tx</tspan></text>
<path d="M-37 1827.2H999" class="inline_expression_divider"/>
<path d="M396.41 1819.95h169.17v14H396.41z" class="label-text-background"/>
<text x="481" y="1830.95" class="empty-text comment-row-text"><tspan>offerer swipes script coin lock tx</tspan></text>
<path marker-end="url(#mscgenjsreplacememethod-#FF0000)" d="M481 1906.12c104 .1 104 22.8 0 22.8" class="arc directional method" style="stroke:red"/>
<path d="M484 1873.87h40.91v14H484z" class="label-text-background"/>
<text x="484" y="1884.87" class="directional-text method-text anchor-start"><tspan>Wait for</tspan></text>
<path d="M484 1889.87h142.39v14H484z" class="label-text-background"/>
<text x="484" y="1900.87" class="directional-text method-text anchor-start"><tspan>pre-refund tx lock to expire</tspan></text>
<path marker-end="url(#mscgenjsreplacemecallback-#FF0000)" d="M481 1962.25H273" class="arc directional callback" style="stroke:red"/>
<path d="M359.98 1946.5h34.03v14h-34.03z" class="label-text-background"/>
<text x="377" y="1957.5" class="directional-text callback-text"><tspan>Sends</tspan></text>
<path d="M283.3 1964.5h187.39v14H283.3z" class="label-text-background"/>
<text x="377" y="1975.5" class="directional-text callback-text"><tspan>script-coin-lock-pre-refund-swipe-tx</tspan></text>
</g>
<g id="mscgenjsreplaceme_notes">
<path d="m383 209 3-17h190l3 17-3 17H386z" class="box abox" style="stroke:red"/>
@@ -371,50 +375,50 @@
<path d="M799 474h395v9h9m-9-9 9 9v25H799v-34z" class="box note" style="fill:#ffc"/>
<text x="1001" y="486.75" class="box-text note-text"><tspan>The XmrBidLockSpendTxMessage contains the script-coin-lock-tx and</tspan></text>
<text x="1001" y="502.75" class="box-text note-text"><tspan>proof the bidder can sign it.</tspan></text>
<path d="m383 529 3-17h190l3 17-3 17H386z" class="box abox" style="stroke:red"/>
<text x="481" y="532.75" class="box-text abox-text"><tspan>Bid Script coin spend tx valid</tspan></text>
<path d="m383 567 3-17h190l3 17-3 17H386z" class="box abox" style="stroke:red"/>
<text x="481" y="562.75" class="box-text abox-text"><tspan>Exchanged script lock spend tx</tspan></text>
<text x="481" y="578.75" class="box-text abox-text"><tspan>msg</tspan></text>
<path d="m383 680.05 3-17h190l3 17-3 17H386z" class="box abox" style="stroke:red"/>
<text x="481" y="683.8" class="box-text abox-text"><tspan>Bid Script coin locked</tspan></text>
<path d="m383 831.1 3-17h190l3 17-3 17H386z" class="box abox" style="stroke:red"/>
<text x="481" y="834.85" class="box-text abox-text"><tspan>Bid Scriptless coin locked</tspan></text>
<path d="M-40 869.1h98.39v11l-7 7H-40" class="box inline_expression_label"/>
<text x="-38" y="882.35" class="inline_expression-text alt-text anchor-start"><tspan>alt: success path</tspan></text>
<path d="M799 890.1h395v9h9m-9-9 9 9v73H799v-82z" class="box note" style="fill:#ffc"/>
<text x="1001" y="902.85" class="box-text note-text"><tspan>The XmrBidLockReleaseMessage contains the bidder&apos;s OTVES for it. </tspan></text>
<text x="1001" y="918.85" class="box-text note-text"><tspan> The offerer decodes the bidder&apos;s signature</tspan></text>
<text x="1001" y="934.85" class="box-text note-text"><tspan>from the OTVES. When the bidder has the</tspan></text>
<text x="1001" y="950.85" class="box-text note-text"><tspan>plaintext signature, they can decode the offerer&apos;s noscript-coin-lock-tx</tspan></text>
<text x="1001" y="966.85" class="box-text note-text"><tspan>signature.</tspan></text>
<path d="m383 993.1 3-17h190l3 17-3 17H386z" class="box abox" style="stroke:red"/>
<text x="481" y="996.85" class="box-text abox-text"><tspan>Script coin lock released</tspan></text>
<path d="m383 1069.1 3-17h190l3 17-3 17H386z" class="box abox" style="stroke:red"/>
<text x="481" y="1072.85" class="box-text abox-text"><tspan>Script tx redeemed</tspan></text>
<text x="481" y="570.75" class="box-text abox-text"><tspan>Bid Script coin spend tx valid</tspan></text>
<path d="m383 605 3-17h190l3 17-3 17H386z" class="box abox" style="stroke:red"/>
<text x="481" y="600.75" class="box-text abox-text"><tspan>Exchanged script lock spend tx</tspan></text>
<text x="481" y="616.75" class="box-text abox-text"><tspan>msg</tspan></text>
<path d="m383 718.05 3-17h190l3 17-3 17H386z" class="box abox" style="stroke:red"/>
<text x="481" y="721.8" class="box-text abox-text"><tspan>Bid Script coin locked</tspan></text>
<path d="m383 869.1 3-17h190l3 17-3 17H386z" class="box abox" style="stroke:red"/>
<text x="481" y="872.85" class="box-text abox-text"><tspan>Bid Scriptless coin locked</tspan></text>
<path d="M-40 907.1h98.39v11l-7 7H-40" class="box inline_expression_label"/>
<text x="-38" y="920.35" class="inline_expression-text alt-text anchor-start"><tspan>alt: success path</tspan></text>
<path d="M799 928.1h395v9h9m-9-9 9 9v73H799v-82z" class="box note" style="fill:#ffc"/>
<text x="1001" y="940.85" class="box-text note-text"><tspan>The XmrBidLockReleaseMessage contains the bidder&apos;s OTVES for it. </tspan></text>
<text x="1001" y="956.85" class="box-text note-text"><tspan> The offerer decodes the bidder&apos;s signature</tspan></text>
<text x="1001" y="972.85" class="box-text note-text"><tspan>from the OTVES. When the bidder has the</tspan></text>
<text x="1001" y="988.85" class="box-text note-text"><tspan>plaintext signature, they can decode the offerer&apos;s noscript-coin-lock-tx</tspan></text>
<text x="1001" y="1004.85" class="box-text note-text"><tspan>signature.</tspan></text>
<path d="m383 1031.1 3-17h190l3 17-3 17H386z" class="box abox" style="stroke:red"/>
<text x="481" y="1034.85" class="box-text abox-text"><tspan>Script coin lock released</tspan></text>
<path d="m383 1107.1 3-17h190l3 17-3 17H386z" class="box abox" style="stroke:red"/>
<text x="481" y="1110.85" class="box-text abox-text"><tspan>Bid Completed</tspan></text>
<path d="M799 1279.15h395v9h9m-9-9 9 9v25H799v-34z" class="box note" style="fill:#ffc"/>
<text x="1001" y="1299.9" class="box-text note-text"><tspan>tx can be sent by either party.</tspan></text>
<path d="m383 1372.15 3-17h190l3 17-3 17H386z" class="box abox" style="stroke:red"/>
<text x="481" y="1367.9" class="box-text abox-text"><tspan>Bid Script pre-refund tx in</tspan></text>
<text x="481" y="1383.9" class="box-text abox-text"><tspan>chain</tspan></text>
<path d="M-36 1410.15h199.77v11l-7 7H-36" class="box inline_expression_label"/>
<text x="-34" y="1423.4" class="inline_expression-text alt-text anchor-start"><tspan>alt: bidder refunds script coin lock tx</tspan></text>
<path d="M799 1528.2h395v9h9m-9-9 9 9v41H799v-50z" class="box note" style="fill:#ffc"/>
<text x="1001" y="1540.95" class="box-text note-text"><tspan>Refunds the script lock tx, with the bidder&apos;s cleartext signature</tspan></text>
<text x="1001" y="1556.95" class="box-text note-text"><tspan>the offerer can refund the noscript lock tx. </tspan></text>
<text x="1001" y="1572.95" class="box-text note-text"><tspan>Once the lock expires the pre-refund tx can be spent by the offerer.</tspan></text>
<path d="m591 1599.2 3-17h190l3 17-3 17H594z" class="box abox" style="stroke:#00f"/>
<text x="689" y="1602.95" class="box-text abox-text"><tspan>Bid Failed, refunded</tspan></text>
<path d="M799 1620.2h395v9h9m-9-9 9 9v25H799v-34z" class="box note" style="fill:#ffc"/>
<text x="1001" y="1640.95" class="box-text note-text"><tspan>offerer recovers the bidder&apos;s scriptless chain key-shard.</tspan></text>
<path d="m383 1713.2 3-17h190l3 17-3 17H386z" class="box abox" style="stroke:red"/>
<text x="481" y="1716.95" class="box-text abox-text"><tspan>Bid Scriptless tx recovered</tspan></text>
<text x="481" y="1110.85" class="box-text abox-text"><tspan>Script tx redeemed</tspan></text>
<path d="m383 1145.1 3-17h190l3 17-3 17H386z" class="box abox" style="stroke:red"/>
<text x="481" y="1148.85" class="box-text abox-text"><tspan>Bid Completed</tspan></text>
<path d="M799 1317.15h395v9h9m-9-9 9 9v25H799v-34z" class="box note" style="fill:#ffc"/>
<text x="1001" y="1337.9" class="box-text note-text"><tspan>tx can be sent by either party.</tspan></text>
<path d="m383 1410.15 3-17h190l3 17-3 17H386z" class="box abox" style="stroke:red"/>
<text x="481" y="1405.9" class="box-text abox-text"><tspan>Bid Script pre-refund tx in</tspan></text>
<text x="481" y="1421.9" class="box-text abox-text"><tspan>chain</tspan></text>
<path d="M-36 1448.15h199.77v11l-7 7H-36" class="box inline_expression_label"/>
<text x="-34" y="1461.4" class="inline_expression-text alt-text anchor-start"><tspan>alt: bidder refunds script coin lock tx</tspan></text>
<path d="M799 1566.2h395v9h9m-9-9 9 9v41H799v-50z" class="box note" style="fill:#ffc"/>
<text x="1001" y="1578.95" class="box-text note-text"><tspan>Refunds the script lock tx, with the bidder&apos;s cleartext signature</tspan></text>
<text x="1001" y="1594.95" class="box-text note-text"><tspan>the offerer can refund the noscript lock tx. </tspan></text>
<text x="1001" y="1610.95" class="box-text note-text"><tspan>Once the lock expires the pre-refund tx can be spent by the offerer.</tspan></text>
<path d="m591 1637.2 3-17h190l3 17-3 17H594z" class="box abox" style="stroke:#00f"/>
<text x="689" y="1640.95" class="box-text abox-text"><tspan>Bid Failed, refunded</tspan></text>
<path d="M799 1658.2h395v9h9m-9-9 9 9v25H799v-34z" class="box note" style="fill:#ffc"/>
<text x="1001" y="1678.95" class="box-text note-text"><tspan>offerer recovers the bidder&apos;s scriptless chain key-shard.</tspan></text>
<path d="m383 1751.2 3-17h190l3 17-3 17H386z" class="box abox" style="stroke:red"/>
<text x="481" y="1754.95" class="box-text abox-text"><tspan>Bid Failed, refunded</tspan></text>
<path d="m383 1962.25 3-17h190l3 17-3 17H386z" class="box abox" style="stroke:red"/>
<text x="481" y="1966" class="box-text abox-text"><tspan>Bid Failed, swiped</tspan></text>
<text x="481" y="1754.95" class="box-text abox-text"><tspan>Bid Scriptless tx recovered</tspan></text>
<path d="m383 1789.2 3-17h190l3 17-3 17H386z" class="box abox" style="stroke:red"/>
<text x="481" y="1792.95" class="box-text abox-text"><tspan>Bid Failed, refunded</tspan></text>
<path d="m383 2000.25 3-17h190l3 17-3 17H386z" class="box abox" style="stroke:red"/>
<text x="481" y="2004" class="box-text abox-text"><tspan>Bid Failed, swiped</tspan></text>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 40 KiB

View File

@@ -1,4 +1,6 @@
{% include 'header.html' %}
{% from 'style.html' import input_arrow_down_svg %}
<div class="container mx-auto">
<section class="p-5 mt-5">
<div class="flex flex-wrap items-center -m-2">
@@ -503,10 +505,7 @@
<tr class="opacity-100 text-gray-500 dark:text-gray-100">
<td class="py-3 px-6 bold">View Transaction:</td>
<td class="py-3 px-6 bold">
<div class="relative">
<svg class="absolute right-4 top-1/2 transform -translate-y-1/2" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.3333 6.1133C11.2084 5.98913 11.0395 5.91943 10.8633 5.91943C10.6872 5.91943 10.5182 5.98913 10.3933 6.1133L8.00001 8.47329L5.64001 6.1133C5.5151 5.98913 5.34613 5.91943 5.17001 5.91943C4.99388 5.91943 4.82491 5.98913 4.70001 6.1133C4.63752 6.17527 4.58792 6.249 4.55408 6.33024C4.52023 6.41148 4.50281 6.49862 4.50281 6.58663C4.50281 6.67464 4.52023 6.76177 4.55408 6.84301C4.58792 6.92425 4.63752 6.99799 4.70001 7.05996L7.52667 9.88663C7.58865 9.94911 7.66238 9.99871 7.74362 10.0326C7.82486 10.0664 7.912 10.0838 8.00001 10.0838C8.08801 10.0838 8.17515 10.0664 8.25639 10.0326C8.33763 9.99871 8.41136 9.94911 8.47334 9.88663L11.3333 7.05996C11.3958 6.99799 11.4454 6.92425 11.4793 6.84301C11.5131 6.76177 11.5305 6.67464 11.5305 6.58663C11.5305 6.49862 11.5131 6.41148 11.4793 6.33024C11.4454 6.249 11.3958 6.17527 11.3333 6.1133Z" fill="#8896AB"></path>
</svg>
<div class="relative">{{ input_arrow_down_svg | safe }}
<select class="bg-gray-50 text-gray-900 appearance-none pr-10 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-50 text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-0" name="view_tx">
{% if data.txns|length %} {% for tx in data.txns %}
<option value="{{ tx.txid }}" {% if data.view_tx_ind==tx.txid %} selected{% endif %}>{{ tx.type }} {{ tx.txid }}</option>
@@ -723,10 +722,7 @@
<tr class="opacity-100 text-gray-500 dark:text-gray-100">
<td class="py-3 px-6 bold">Change Bid State:</td>
<td class="py-3 px-6">
<div class="relative">
<svg class="absolute right-4 top-1/2 transform -translate-y-1/2" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.3333 6.1133C11.2084 5.98913 11.0395 5.91943 10.8633 5.91943C10.6872 5.91943 10.5182 5.98913 10.3933 6.1133L8.00001 8.47329L5.64001 6.1133C5.5151 5.98913 5.34613 5.91943 5.17001 5.91943C4.99388 5.91943 4.82491 5.98913 4.70001 6.1133C4.63752 6.17527 4.58792 6.249 4.55408 6.33024C4.52023 6.41148 4.50281 6.49862 4.50281 6.58663C4.50281 6.67464 4.52023 6.76177 4.55408 6.84301C4.58792 6.92425 4.63752 6.99799 4.70001 7.05996L7.52667 9.88663C7.58865 9.94911 7.66238 9.99871 7.74362 10.0326C7.82486 10.0664 7.912 10.0838 8.00001 10.0838C8.08801 10.0838 8.17515 10.0664 8.25639 10.0326C8.33763 9.99871 8.41136 9.94911 8.47334 9.88663L11.3333 7.05996C11.3958 6.99799 11.4454 6.92425 11.4793 6.84301C11.5131 6.76177 11.5305 6.67464 11.5305 6.58663C11.5305 6.49862 11.5131 6.41148 11.4793 6.33024C11.4454 6.249 11.3958 6.17527 11.3333 6.1133Z" fill="#8896AB"></path>
</svg>
<div class="relative">{{ input_arrow_down_svg | safe }}
<select class="bg-gray-50 text-gray-900 appearance-none pr-10 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-50 text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-0" name="new_state">
{% for s in data.bid_states %}
<option value="{{ s[0] }}" {% if data.bid_state_ind==s[0] %} selected{% endif %}>{{ s[1] }}</option>
@@ -736,13 +732,22 @@
</td>
</tr>
{% if data.debug_ui == true %}
<tr class="opacity-100 text-gray-500 dark:text-gray-100">
<td class="py-3 px-6 bold">Add Bid Action:</td>
<td class="py-3 px-6">
<div class="relative">{{ input_arrow_down_svg | safe }}
<select class="bg-gray-50 text-gray-900 appearance-none pr-10 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-50 text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-0" name="new_action">
{% for a in data.bid_actions %}
<option value="{{ a[0] }}">{{ a[1] }}</option>
{% endfor %}
</select>
</div>
</td>
</tr>
<tr class="opacity-100 text-gray-500 dark:text-gray-100">
<td class="py-3 px-6 bold">Debug Option</td>
<td class="py-3 px-6">
<div class="relative">
<svg class="absolute right-4 top-1/2 transform -translate-y-1/2" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.3333 6.1133C11.2084 5.98913 11.0395 5.91943 10.8633 5.91943C10.6872 5.91943 10.5182 5.98913 10.3933 6.1133L8.00001 8.47329L5.64001 6.1133C5.5151 5.98913 5.34613 5.91943 5.17001 5.91943C4.99388 5.91943 4.82491 5.98913 4.70001 6.1133C4.63752 6.17527 4.58792 6.249 4.55408 6.33024C4.52023 6.41148 4.50281 6.49862 4.50281 6.58663C4.50281 6.67464 4.52023 6.76177 4.55408 6.84301C4.58792 6.92425 4.63752 6.99799 4.70001 7.05996L7.52667 9.88663C7.58865 9.94911 7.66238 9.99871 7.74362 10.0326C7.82486 10.0664 7.912 10.0838 8.00001 10.0838C8.08801 10.0838 8.17515 10.0664 8.25639 10.0326C8.33763 9.99871 8.41136 9.94911 8.47334 9.88663L11.3333 7.05996C11.3958 6.99799 11.4454 6.92425 11.4793 6.84301C11.5131 6.76177 11.5305 6.67464 11.5305 6.58663C11.5305 6.49862 11.5131 6.41148 11.4793 6.33024C11.4454 6.249 11.3958 6.17527 11.3333 6.1133Z" fill="#8896AB"></path>
</svg>
<div class="relative">{{ input_arrow_down_svg | safe }}
<select class="bg-gray-50 text-gray-900 appearance-none pr-10 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-50 text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-0" name="debugind">
<option{% if data.debug_ind=="-1" %} selected{% endif %} value="-1">None</option>
{% for a in data.debug_options %}

View File

@@ -21,9 +21,9 @@
<div class="w-full md:w-1/2 mb-6 md:mb-0">
<div class="flex items-center">
<div class="flex items-center">
<p class="text-sm text-gray-90 dark:text-white font-medium">© 2023~ (BSX) BasicSwap</p> <span class="w-1 h-1 mx-1.5 bg-gray-500 dark:bg-white rounded-full"></span>
<p class="text-sm text-gray-90 dark:text-white font-medium">© 2024~ (BSX) BasicSwap</p> <span class="w-1 h-1 mx-1.5 bg-gray-500 dark:bg-white rounded-full"></span>
<p class="text-sm text-coolGray-400 font-medium">BSX: v{{ version }}</p> <span class="w-1 h-1 mx-1.5 bg-gray-500 dark:bg-white rounded-full"></span>
<p class="text-sm text-coolGray-400 font-medium">GUI: v2.0.2</p> <span class="w-1 h-1 mx-1.5 bg-gray-500 dark:bg-white rounded-full"></span>
<p class="text-sm text-coolGray-400 font-medium">GUI: v2.0.3</p> <span class="w-1 h-1 mx-1.5 bg-gray-500 dark:bg-white rounded-full"></span>
<p class="mr-2 text-sm font-bold dark:text-white text-gray-90 ">Made with </p>
<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 24 24">
<g stroke-linecap="round" stroke-width="2" fill="none" stroke="#f80b0b" stroke-linejoin="round">

View File

@@ -11,7 +11,7 @@
</svg>
</div>
<div class="flex-1 p-1">
<h3 class="font-medium text-sm text-green-900">{{ m[1] }}</h3></div>
<h3 class="infomsg font-medium text-sm text-green-900">{{ m[1] }}</h3></div>
</div>
</div>
<div class="w-auto p-2">
@@ -45,7 +45,7 @@
</div>
</div>
<div class="w-auto p-2">
<button type="button" class="ml-auto bg-red-100 text-red-500 rounded-lg focus:ring-0 focus:ring-red-400 p-1.5 hover:bg-red-200 inline-flex h-8 w-8 focus:outline-none" data-dismiss-target="#err_messages_{{ err_messages[0][0] }}" aria-label="Close">
<button type="button" class="ml-auto bg-red-100 text-red-500 rounded-lg focus:ring-0 focus:ring-red-400 p-1.5 hover:bg-red-200 inline-flex h-8 w-8 focus:outline-none" data-dismiss-target="#err_messages_{{ err_messages[0][0] }}" aria-label="Close">
<span class="sr-only">Close</span>
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path>
@@ -57,4 +57,4 @@
</div>
</section>
{% endif %}
<!-- todo messages colors better for darkmode -->
<!-- todo messages colors better for darkmode -->

View File

@@ -435,15 +435,14 @@
<div class="flex flex-wrap justify-end">
<!--<div class="w-full md:w-auto p-1.5"><button name="show_rates_table" type="button" value="Show Rates Table" onclick='lookup_rates_table();' class="flex flex-wrap justify-center w-full px-4 py-2.5 font-medium text-sm text-coolGray-500 hover:text-coolGray-600 border border-coolGray-200 hover:border-coolGray-300 bg-white rounded-md shadow-button focus:ring-0 focus:outline-none"><svg class="mr-2"
xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 24 24"><g fill="#556987"><path fill="#556987" d="M12,3c1.989,0,3.873,0.65,5.43,1.833l-3.604,3.393l9.167,0.983L22.562,0l-3.655,3.442 C16.957,1.862,14.545,1,12,1C5.935,1,1,5.935,1,12h2C3,7.037,7.037,3,12,3z"></path><path data-color="#556987" d="M12,21c-1.989,0-3.873-0.65-5.43-1.833l3.604-3.393l-9.167-0.983L1.438,24l3.655-3.442 C7.043,22.138,9.455,23,12,23c6.065,0,11-4.935,11-11h-2C21,16.963,16.963,21,12,21z"></path></g></svg><span>Show Rates Table</span></button></div>-->
{% if show_chart %}
{% if show_chart %}
<div class="w-full md:w-auto p-1.5">
<button name="loadPrices" type="button" value="Lookup Rates (RAW)" onclick="loadPrices();" class="flex flex-wrap justify-center w-full px-4 py-2.5 font-medium text-sm text-coolGray-500 hover:text-coolGray-600 border border-coolGray-200 hover:border-coolGray-300 bg-white rounded-md focus:ring-0 focus:outline-none dark:text-white dark:hover:text-white dark:bg-gray-600 dark:hover:bg-gray-700 dark:border-gray-600 dark:hover:border-gray-600">
<span>Check Current Prices/Rates (TABLE)</span>
</button>
<button name="loadPrices" id="loadPricesButton" type="button" value="Check Current Prices/Rates (TABLE)" class="flex flex-wrap justify-center w-full px-4 py-2.5 font-medium text-sm text-coolGray-500 hover:text-coolGray-600 border border-coolGray-200 hover:border-coolGray-300 bg-white rounded-md focus:ring-0 focus:outline-none dark:text-white dark:hover:text-white dark:bg-gray-600 dark:hover:bg-gray-700 dark:border-gray-600 dark:hover:border-gray-600"><span>Check Current Prices/Rates (TABLE)</span>
</button>
</div>
{% endif %}
<div class="w-full md:w-auto p-1.5">
<button name="check_rates" type="button" value="Lookup Rates (RAW)" onclick='lookup_rates();' class="flex flex-wrap justify-center w-full px-4 py-2.5 font-medium text-sm text-coolGray-500 hover:text-coolGray-600 border border-coolGray-200 hover:border-coolGray-300 bg-white rounded-md focus:ring-0 focus:outline-none dark:text-white dark:hover:text-white dark:bg-gray-600 dark:hover:bg-gray-700 dark:border-gray-600 dark:hover:border-gray-600"><span>Check Current Prices/Rates (JSON)</span>
<button name="check_rates" type="button" value="Check Current Prices/Rates (JSON)" onclick='lookup_rates();' class="flex flex-wrap justify-center w-full px-4 py-2.5 font-medium text-sm text-coolGray-500 hover:text-coolGray-600 border border-coolGray-200 hover:border-coolGray-300 bg-white rounded-md focus:ring-0 focus:outline-none dark:text-white dark:hover:text-white dark:bg-gray-600 dark:hover:bg-gray-700 dark:border-gray-600 dark:hover:border-gray-600"><span>Check Current Prices/Rates (JSON)</span>
</button>
</div>
<div class="w-full md:w-auto p-1.5">
@@ -540,22 +539,34 @@ xhr_rates_table.onload = () => {
function lookup_rates() {
const coin_from = document.getElementById('coin_from').value;
const coin_to = document.getElementById('coin_to').value;
if (coin_from == '-1' || coin_to == '-1') {
if (coin_from === '-1' || coin_to === '-1') {
alert('Coins from and to must be set first.');
return;
}
const selectedCoin = (coin_from === '15') ? '3' : coin_from;
inner_html = '<p>Updating...</p>';
document.getElementById('rates_display').innerHTML = inner_html;
// Remove the 'hidden' class
document.querySelector(".pricejsonhidden").classList.remove("hidden");
const xhr_rates = new XMLHttpRequest();
xhr_rates.onreadystatechange = function() {
if (xhr_rates.readyState === XMLHttpRequest.DONE) {
if (xhr_rates.status === 200) {
document.getElementById('rates_display').innerHTML = xhr_rates.responseText;
} else {
console.error('Error fetching data:', xhr_rates.statusText);
}
}
};
xhr_rates.open('POST', '/json/rates');
xhr_rates.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
xhr_rates.send('coin_from=' + coin_from + '&coin_to=' + coin_to);
xhr_rates.send('coin_from=' + selectedCoin + '&coin_to=' + coin_to);
}
function lookup_rates_table() {
const coin_from = document.getElementById('coin_from').value;
const coin_to = document.getElementById('coin_to').value;
@@ -575,8 +586,8 @@ function lookup_rates_table() {
function set_swap_type_enabled(coin_from, coin_to, swap_type) {
const adaptor_sig_only_coins = ['6' /* XMR */, '8' /* PART_ANON */, '7' /* PART_BLIND */];
const secret_hash_only_coins = ['11' /* PIVX */, '12' /* DASH */, '13' /* FIRO */];
const adaptor_sig_only_coins = ['6' /* XMR */, '8' /* PART_ANON */, '7' /* PART_BLIND */, '13' /* FIRO */];
const secret_hash_only_coins = ['11' /* PIVX */, '12' /* DASH */];
let make_hidden = false;
if (adaptor_sig_only_coins.includes(coin_from) || adaptor_sig_only_coins.includes(coin_to)) {
swap_type.disabled = true;
@@ -584,7 +595,7 @@ function set_swap_type_enabled(coin_from, coin_to, swap_type) {
make_hidden = true;
swap_type.classList.add('select-disabled'); // Add the class to the disabled select
} else
if (secret_hash_only_coins.includes(coin_from)) {
if (secret_hash_only_coins.includes(coin_from) && secret_hash_only_coins.includes(coin_to)) {
swap_type.disabled = true;
swap_type.value = 'seller_first';
make_hidden = true;
@@ -652,17 +663,10 @@ document.addEventListener("DOMContentLoaded", function() {
const swap_type = document.getElementById('swap_type');
set_swap_type_enabled(coin_from, coin_to, swap_type);
});
</script>
<script src="static/js/new_offer.js"></script>
<script src="static/js/coin_icons.js"></script>
<script src="static/js/coin_icons_2.js"></script>
</div>
{% include 'footer.html' %}
</div>
{% if show_chart %}
<script>
document.addEventListener('DOMContentLoaded', function() {
document.querySelector("button[name='loadPrices']").addEventListener("click", loadPrices);
const loadPricesButton = document.getElementById("loadPricesButton");
function loadPrices() {
const api_key = '{{chart_api_key}}';
@@ -678,25 +682,27 @@ document.addEventListener('DOMContentLoaded', function() {
const priceBTC = data.RAW[coin].BTC.PRICE;
const tableRow = document.createElement("tr");
tableRow.classList.add("opacity-100", "text-gray-500", "dark:text-gray-100", "dark:text-gray-100", "hover:bg-coolGray-200", "dark:hover:bg-gray-600");
tableRow.classList.add("opacity-100", "text-gray-500", "dark:text-gray-100",
"dark:text-gray-100", "hover:bg-coolGray-200",
"dark:hover:bg-gray-600");
const coinCell = document.createElement("td", "py-3", "px-6");
const coinCell = document.createElement("td");
coinCell.textContent = coin;
coinCell.classList.add("py-3", "px-6", "bold");
tableRow.appendChild(coinCell);
const usdPriceCell = document.createElement("td", "py-3", "px-6");
const usdPriceCell = document.createElement("td");
usdPriceCell.textContent = priceUSD.toFixed(2) + ' USD';
coinCell.classList.add("py-3", "px-6");
usdPriceCell.classList.add("py-3");
tableRow.appendChild(usdPriceCell);
const btcPriceCell = document.createElement("td");
btcPriceCell.classList.add("py-3");
if (coin !== 'BTC') {
btcPriceCell.textContent = priceBTC.toFixed(8) + ' BTC';
} else {
btcPriceCell.textContent = '-';
}
coinCell.classList.add("py-3", "px-6");
tableRow.appendChild(btcPriceCell);
document.getElementById("priceTableBody").appendChild(tableRow);
@@ -704,15 +710,19 @@ document.addEventListener('DOMContentLoaded', function() {
.catch(error => console.error(`Error fetching ${coin} data:`, error));
});
// Remove the 'hidden' class from the section when the button is clicked
document.querySelector(".pricetablehidden").classList.remove("hidden");
// Disable the button to prevent multiple clicks
const button = document.querySelector("button[name='loadPrices']");
button.disabled = true;
loadPricesButton.disabled = true;
}
loadPricesButton.addEventListener("click", loadPrices);
});
</script>
{% endif %}
</script>
<script src="static/js/new_offer.js"></script>
<script src="static/js/coin_icons.js"></script>
<script src="static/js/coin_icons_2.js"></script>
</div>
{% include 'footer.html' %}
</div>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -121,7 +121,7 @@
<div class="px-6">
<div class="flex flex-wrap justify-end">
<div class="w-full md:w-auto p-1.5 ml-2">
<button name="" value="Apply" type="submit" class="flex flex-wrap justify-center w-full px-4 py-2.5 bg-blue-500 hover:bg-blue-600 font-medium text-sm text-white border border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none">
<button name="apply" value="Apply" type="submit" class="flex flex-wrap justify-center w-full px-4 py-2.5 bg-blue-500 hover:bg-blue-600 font-medium text-sm text-white border border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none">
<svg class="text-gray-500 w-5 h-5 mr-2" xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 24 24">
<g stroke-linecap="round" stroke-width="2" fill="none" stroke="#ffffff" stroke-linejoin="round">
<polyline points=" 6,12 10,16 18,8 " stroke="#ffffff"></polyline>
@@ -160,7 +160,7 @@
</thead>
<tr class="opacity-100 text-gray-500 dark:text-gray-100">
<td class="py-3 px-6">
<textarea class="hover:border-blue-500 bg-gray-50 text-gray-900 appearance-none pr-10 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-50 text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-0" rows="20">{{ result }}</textarea>
<textarea name="result" class="hover:border-blue-500 bg-gray-50 text-gray-900 appearance-none pr-10 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-50 text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-0" rows="20">{{ result }}</textarea>
</td>
</tr>
</table>

View File

@@ -48,7 +48,7 @@
<div class="text-sm font-medium text-center text-gray-500 border-b border-gray-200 dark:text-gray-400 dark:border-gray-700">
<ul class="flex flex-wrap -mb-px" id="myTab" data-tabs-toggle="#settingstab" role="tablist">
<li class="mr-2" role="presentation">
<a class="inline-block p-4 border-b-2 border-transparent rounded-t-lg hover:text-gray-600 hover:border-gray-300 dark:hover:text-gray-300" id="profile-tab" data-tabs-target="#coins" role="tab" aria-controls="coins" aria-selected={% if active_tab == 'default' %}"true"{% else %}"false"{% endif %}>Coins</a>
<a class="inline-block p-4 border-b-2 border-transparent rounded-t-lg hover:text-gray-600 hover:border-gray-300 dark:hover:text-gray-300" id="coins-tab" data-tabs-target="#coins" role="tab" aria-controls="coins" aria-selected={% if active_tab == 'default' %}"true"{% else %}"false"{% endif %}>Coins</a>
</li>
<li class="mr-2" role="presentation">
<a class="inline-block p-4 border-b-2 border-transparent rounded-t-lg hover:text-gray-600 hover:border-gray-300 dark:hover:text-gray-300" id="general-tab" data-tabs-target="#general" role="tab" aria-controls="general" aria-selected={% if active_tab == 'general' %}"true"{% else %}"false"{% endif %}>General</a>

View File

@@ -0,0 +1,118 @@
{% set select_box_arrow_svg = '<svg class="absolute right-4 top-1/2 transform -translate-y-1/2" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.3333 6.1133C11.2084 5.98913 11.0395 5.91943 10.8633 5.91943C10.6872 5.91943 10.5182 5.98913 10.3933 6.1133L8.00001 8.47329L5.64001 6.1133C5.5151 5.98913 5.34613 5.91943 5.17001 5.91943C4.99388 5.91943 4.82491 5.98913 4.70001 6.1133C4.63752 6.17527 4.58792 6.249 4.55408 6.33024C4.52023 6.41148 4.50281 6.49862 4.50281 6.58663C4.50281 6.67464 4.52023 6.76177 4.55408 6.84301C4.58792 6.92425 4.63752 6.99799 4.70001 7.05996L7.52667 9.88663C7.58865 9.94911 7.66238 9.99871 7.74362 10.0326C7.82486 10.0664 7.912 10.0838 8.00001 10.0838C8.08801 10.0838 8.17515 10.0664 8.25639 10.0326C8.33763 9.99871 8.41136 9.94911 8.47334 9.88663L11.3333 7.05996C11.3958 6.99799 11.4454 6.92425 11.4793 6.84301C11.5131 6.76177 11.5305 6.67464 11.5305 6.58663C11.5305 6.49862 11.5131 6.41148 11.4793 6.33024C11.4454 6.249 11.3958 6.17527 11.3333 6.1133Z" fill="#8896AB"></path>
</svg>' %}
{% set circular_arrows_svg = '<svg class="text-gray-500 w-5 h-5 mr-2" xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 24 24">
<g fill="#ffffff" class="nc-icon-wrapper">
<path fill="#ffffff" d="M12,3c1.989,0,3.873,0.65,5.43,1.833l-3.604,3.393l9.167,0.983L22.562,0l-3.655,3.442C16.957,1.862,14.545,1,12,1C5.935,1,1,5.935,1,12h2C3,7.037,7.037,3,12,3z"></path>
<path data-color="color-2" d="M12,21c-1.989,0-3.873-0.65-5.43-1.833l3.604-3.393l-9.167-0.983L1.438,24l3.655-3.442C7.043,22.138,9.455,23,12,23c6.065,0,11-4.935,11-11h-2C21,16.963,16.963,21,12,21z"></path>
</g>
</svg>' %}
{% set circular_error_svg = '<svg class="relative top-0.5" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.4733 5.52667C10.4114 5.46419 10.3376 5.41459 10.2564 5.38075C10.1751 5.3469 10.088 5.32947 9.99999 5.32947C9.91198 5.32947 9.82485 5.3469 9.74361 5.38075C9.66237 5.41459 9.58863 5.46419 9.52666 5.52667L7.99999 7.06001L6.47333 5.52667C6.34779 5.40114 6.17753 5.33061 5.99999 5.33061C5.82246 5.33061 5.65219 5.40114 5.52666 5.52667C5.40112 5.65221 5.3306 5.82247 5.3306 6.00001C5.3306 6.17754 5.40112 6.3478 5.52666 6.47334L7.05999 8.00001L5.52666 9.52667C5.46417 9.58865 5.41458 9.66238 5.38073 9.74362C5.34689 9.82486 5.32946 9.912 5.32946 10C5.32946 10.088 5.34689 10.1752 5.38073 10.2564C5.41458 10.3376 5.46417 10.4114 5.52666 10.4733C5.58863 10.5358 5.66237 10.5854 5.74361 10.6193C5.82485 10.6531 5.91198 10.6705 5.99999 10.6705C6.088 10.6705 6.17514 10.6531 6.25638 10.6193C6.33762 10.5854 6.41135 10.5358 6.47333 10.4733L7.99999 8.94001L9.52666 10.4733C9.58863 10.5358 9.66237 10.5854 9.74361 10.6193C9.82485 10.6531 9.91198 10.6705 9.99999 10.6705C10.088 10.6705 10.1751 10.6531 10.2564 10.6193C10.3376 10.5854 10.4114 10.5358 10.4733 10.4733C10.5358 10.4114 10.5854 10.3376 10.6193 10.2564C10.6531 10.1752 10.6705 10.088 10.6705 10C10.6705 9.912 10.6531 9.82486 10.6193 9.74362C10.5854 9.66238 10.5358 9.58865 10.4733 9.52667L8.93999 8.00001L10.4733 6.47334C10.5358 6.41137 10.5854 6.33763 10.6193 6.25639C10.6531 6.17515 10.6705 6.08802 10.6705 6.00001C10.6705 5.912 10.6531 5.82486 10.6193 5.74362C10.5854 5.66238 10.5358 5.58865 10.4733 5.52667ZM12.7133 3.28667C12.0983 2.64994 11.3627 2.14206 10.5494 1.79266C9.736 1.44327 8.8612 1.25936 7.976 1.25167C7.0908 1.24398 6.21294 1.41266 5.39363 1.74786C4.57432 2.08307 3.82998 2.57809 3.20403 3.20404C2.57807 3.82999 2.08305 4.57434 1.74785 5.39365C1.41264 6.21296 1.24396 7.09082 1.25166 7.97602C1.25935 8.86121 1.44326 9.73601 1.79265 10.5494C2.14204 11.3627 2.64992 12.0984 3.28666 12.7133C3.90164 13.3501 4.63727 13.858 5.45063 14.2074C6.26399 14.5567 7.13879 14.7407 8.02398 14.7483C8.90918 14.756 9.78704 14.5874 10.6064 14.2522C11.4257 13.9169 12.17 13.4219 12.796 12.796C13.4219 12.17 13.9169 11.4257 14.2521 10.6064C14.5873 9.78706 14.756 8.90919 14.7483 8.024C14.7406 7.1388 14.5567 6.264 14.2073 5.45064C13.8579 4.63728 13.3501 3.90165 12.7133 3.28667ZM11.7733 11.7733C10.9014 12.6463 9.75368 13.1899 8.52585 13.3115C7.29802 13.4332 6.066 13.1254 5.03967 12.4405C4.01335 11.7557 3.25623 10.7361 2.89731 9.55566C2.53838 8.37518 2.59986 7.10677 3.07127 5.96653C3.54267 4.82629 4.39484 3.88477 5.48259 3.30238C6.57033 2.71999 7.82635 2.53276 9.03666 2.77259C10.247 3.01242 11.3367 3.66447 12.1202 4.61765C12.9036 5.57083 13.3324 6.76617 13.3333 8.00001C13.3357 8.70087 13.1991 9.39524 12.9313 10.0429C12.6635 10.6906 12.2699 11.2788 11.7733 11.7733Z" fill="#EF5944"></path>
</svg>' %}
{% set circular_info_svg = '<svg aria-hidden="true" class="flex-shrink-0 w-5 h-5 text-blue-700 dark:text-blue-800" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"></path>
</svg>' %}
{% set cross_close_svg = '<svg aria-hidden="true" class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path>
</svg>' %}
{% set breadcrumb_line_svg = '<svg width="6" height="15" viewBox="0 0 6 15" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M5.34 0.671999L2.076 14.1H0.732L3.984 0.671999H5.34Z" fill="#BBC3CF"></path></svg>' %}
{% set withdraw_svg = '<svg class="text-gray-500 w-5 h-5 mr-2" xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 24 24"><g fill="#ffffff" class="nc-icon-wrapper"><polygon data-color="color-2" points="6,10 12,17 18,10 13,10 13,1 11,1 11,10 "></polygon><path fill="#ffffff" d="M22,21H2v-6H0v7c0,0.552,0.448,1,1,1h22c0.552,0,1-0.448,1-1v-7h-2V21z"></path></g></svg>' %}
{% set utxo_groups_svg = '<svg class="text-gray-500 w-5 h-5 mr-2" xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 24 24">
<g stroke-linecap="round" stroke-width="2" fill="none" stroke="#ffffff" stroke-linejoin="round" class="nc-icon-wrapper">
<rect x="2" y="9" width="12" height="14"></rect>
<polyline points=" 6,5 18,5 18,19 " stroke="#ffffff"></polyline>
<polyline points=" 10,1 22,1 22,15 " stroke="#ffffff"></polyline>
</g>
</svg>' %}
{% set create_utxo_svg = '<svg class="text-gray-500 w-5 h-5 mr-2" xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 24 24">
<g stroke-linecap="round" stroke-width="2" fill="none" stroke="#ffffff" stroke-linejoin="round" class="nc-icon-wrapper">
<polyline points="1 15 1 21 23 21 23 15"></polyline>
<line x1="12" y1="3" x2="12" y2="13" stroke="#ffffff"></line>
<line x1="17" y1="8" x2="7" y2="8" stroke="#ffffff"></line>
</g>
</svg>' %}
{% set lock_svg = '<svg class="text-gray-500 w-5 h-5 mr-2" xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 24 24">
<g fill="#ffffff"><path d="M19,10H5a3,3,0,0,0-3,3v8a3,3,0,0,0,3,3H19a3,3,0,0,0,3-3V13A3,3,0,0,0,19,10Zm-7,9a2,2,0,1,1,2-2A2,2,0,0,1,12,19Z" fill="#ffffff"></path><path data-color="color-2" d="M18,8H16V6a3.958,3.958,0,0,0-3.911-4h-.042A3.978,3.978,0,0,0,8,5.911V8H6V5.9A5.961,5.961,0,0,1,11.949,0h.061A5.979,5.979,0,0,1,18,6.01Z"></path></g>
</svg>
' %}
{% set eye_show_svg = '<svg xmlns="http://www.w3.org/2000/svg" fill="#ffffff" height="20" width="20" viewBox="0 0 24 24">
<path d="M23.444,10.239a22.936,22.936,0,0,0-2.492-2.948l-4.021,4.021A5.026,5.026,0,0,1,17,12a5,5,0,0,1-5,5,5.026,5.026,0,0,1-.688-.069L8.055,20.188A10.286,10.286,0,0,0,12,21c5.708,0,9.905-5.062,11.445-7.24A3.058,3.058,0,0,0,23.444,10.239Z"></path>
<path d="M12,3C6.292,3,2.1,8.062,.555,10.24a3.058,3.058,0,0,0,0,3.52h0a21.272,21.272,0,0,0,4.784,4.9l3.124-3.124a5,5,0,0,1,7.071-7.072L8.464,15.536l10.2-10.2A11.484,11.484,0,0,0,12,3Z"></path>
<path data-color="color-2" d="M1,24a1,1,0,0,1-.707-1.707l22-22a1,1,0,0,1,1.414,1.414l-22,22A1,1,0,0,1,1,24Z"></path>
</svg>
' %}
{% set place_new_offer_svg = '<svg class="text-gray-500 w-5 h-5 mr-2" xmlns="http://www.w3.org/2000/svg" height="18" width="18" viewBox="0 0 24 24">
<g stroke-linecap="round" stroke-width="2" fill="none" stroke="#ffffff" stroke-linejoin="round">
<circle cx="5" cy="5" r="4"></circle>
<circle cx="19" cy="19" r="4"></circle>
<polyline data-cap="butt" points="13,5 21,5 21,11 " stroke="#ffffff"></polyline>
<polyline data-cap="butt" points="11,19 3,19 3,13 " stroke="#ffffff"></polyline>
<polyline points=" 16,2 13,5 16,8 " stroke="#ffffff"></polyline>
<polyline points=" 8,16 11,19 8,22 " stroke="#ffffff"></polyline>
</g>
</svg>
' %}
{% set page_back_svg = '<svg aria-hidden="true" class="mr-2 w-5 h-5" fill="#fff" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z" clip-rule="evenodd"></path>
</svg>
' %}
{% set page_forwards_svg = '<svg aria-hidden="true" class="ml-2 w-5 h-5" fill="#fff" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M12.293 5.293a1 1 0 011.414 0l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-2.293-2.293a1 1 0 010-1.414z" clip-rule="evenodd"></path>
</svg>
' %}
{% set filter_clear_svg = '<svg class="mr-2 w-5 h-5" xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 24 24">
<g stroke-linecap="round" stroke-width="2" fill="none" stroke="#ffffff" stroke-linejoin="round">
<line x1="20" y1="2" x2="12.329" y2="11.506"></line>
<path d="M11,11a2,2,0,0,1,2,2,3.659,3.659,0,0,1-.2.891A9.958,9.958,0,0,0,13.258,23H1C1,16.373,4.373,11,11,11Z"></path>
<line x1="18" y1="15" x2="23" y2="15" stroke="#ffffff"></line>
<line x1="17" y1="19" x2="23" y2="19" stroke="#ffffff"></line>
<line x1="19" y1="23" x2="23" y2="23" stroke="#ffffff"></line>
<path d="M8.059,11.415A3.9,3.9,0,0,0,12,16c.041,0,.079-.011.12-.012" data-cap="butt"></path>
<path d="M5,23a13.279,13.279,0,0,1,.208-3.4" data-cap="butt"></path>
<path d="M9.042,23c-.688-1.083-.313-3.4-.313-3.4" data-cap="butt"></path>
</g>
</svg>
' %}
{% set filter_apply_svg = ' <svg class="mr-2 w-5 h-5" xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 24 24">
<g stroke-linecap="round" stroke-width="2" fill="none" stroke="#ffffff" stroke-linejoin="round">
<rect x="2" y="2" width="7" height="7"></rect>
<rect x="15" y="15" width="7" height="7"></rect>
<rect x="2" y="15" width="7" height="7"></rect>
<polyline points="15 6 17 8 22 3" stroke="#ffffff"></polyline>
</g>
</svg>
' %}
{% set input_arrow_down_svg = '<svg class="absolute right-4 top-1/2 transform -translate-y-1/2" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.3333 6.1133C11.2084 5.98913 11.0395 5.91943 10.8633 5.91943C10.6872 5.91943 10.5182 5.98913 10.3933 6.1133L8.00001 8.47329L5.64001 6.1133C5.5151 5.98913 5.34613 5.91943 5.17001 5.91943C4.99388 5.91943 4.82491 5.98913 4.70001 6.1133C4.63752 6.17527 4.58792 6.249 4.55408 6.33024C4.52023 6.41148 4.50281 6.49862 4.50281 6.58663C4.50281 6.67464 4.52023 6.76177 4.55408 6.84301C4.58792 6.92425 4.63752 6.99799 4.70001 7.05996L7.52667 9.88663C7.58865 9.94911 7.66238 9.99871 7.74362 10.0326C7.82486 10.0664 7.912 10.0838 8.00001 10.0838C8.08801 10.0838 8.17515 10.0664 8.25639 10.0326C8.33763 9.99871 8.41136 9.94911 8.47334 9.88663L11.3333 7.05996C11.3958 6.99799 11.4454 6.92425 11.4793 6.84301C11.5131 6.76177 11.5305 6.67464 11.5305 6.58663C11.5305 6.49862 11.5131 6.41148 11.4793 6.33024C11.4454 6.249 11.3958 6.17527 11.3333 6.1133Z" fill="#8896AB"></path>
</svg>
' %}
{% set arrow_right_svg = ' <svg aria-hidden="true" class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg ">
<path fill-rule="evenodd " d="M12.293 5.293a1 1 0 011.414 0l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-2.293-2.293a1 1 0 010-1.414z " clip-rule="evenodd"></path>
</svg>
' %}
{% set select_box_class = 'hover:border-blue-500 bg-gray-50 text-gray-900 appearance-none pr-10 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-white text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-0' %}

File diff suppressed because it is too large Load Diff

View File

@@ -1,98 +1,71 @@
{% include 'header.html' %}
{% from 'style.html' import breadcrumb_line_svg, circular_arrows_svg, withdraw_svg, utxo_groups_svg, create_utxo_svg, lock_svg, eye_show_svg %}
<div class="container mx-auto">
<section class="p-5 mt-5">
<section class="p-5 mt-5">
<div class="flex flex-wrap items-center -m-2">
<div class="w-full md:w-1/2 p-2">
<ul class="flex flex-wrap items-center gap-x-3 mb-2">
<li>
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/">
<p>Home</p>
</a>
</li>
<li>
<svg width="6" height="15" viewBox="0 0 6 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.34 0.671999L2.076 14.1H0.732L3.984 0.671999H5.34Z" fill="#BBC3CF"></path>
</svg>
</li>
<li>
<a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/wallets">Wallets</a>
</li>
<li>
<svg width="6" height="15" viewBox="0 0 6 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.34 0.671999L2.076 14.1H0.732L3.984 0.671999H5.34Z" fill="#BBC3CF"></path>
</svg>
</li>
</ul>
</div>
<div class="w-full md:w-1/2 p-2">
<ul class="flex flex-wrap items-center gap-x-3 mb-2">
<li><a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/"><p>Home</p></a></li>
<li>{{ breadcrumb_line_svg | safe }}</li>
<li><a class="flex font-medium text-xs text-coolGray-500 dark:text-gray-300 hover:text-coolGray-700" href="/wallets">Wallets</a></li>
<li>{{ breadcrumb_line_svg | safe }}</li>
</ul>
</div>
</div>
</section>
<section class="py-3">
</section>
<section class="py-3">
<div class="container px-4 mx-auto">
<div class="relative py-11 px-16 bg-coolGray-900 dark:bg-blue-500 rounded-md overflow-hidden">
<img class="absolute z-10 left-4 top-4" src="/static/images/elements/dots-red.svg" alt="">
<img class="absolute z-10 right-4 bottom-4" src="/static/images/elements/dots-red.svg" alt="">
<img class="absolute h-64 left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2 object-cover" src="/static/images/elements/wave.svg" alt="">
<img class="absolute z-10 left-4 top-4" src="/static/images/elements/dots-red.svg" alt="dots-red">
<img class="absolute z-10 right-4 bottom-4" src="/static/images/elements/dots-red.svg" alt="dots-red">
<img class="absolute h-64 left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2 object-cover" src="/static/images/elements/wave.svg" alt="wave">
<div class="relative z-20 flex flex-wrap items-center -m-3">
<div class="w-full md:w-1/2 p-3 h-48">
<h2 class="mb-6 text-4xl font-bold text-white tracking-tighter">Wallets</h2>
<div class="flex items-center">
<h2 class="text-lg font-bold text-white tracking-tighter mr-2">Total Assets:</h2>
<button id="hide-usd-amount-toggle" class="flex items-center justify-center p-1 focus:ring-0 focus:outline-none">
<svg xmlns="http://www.w3.org/2000/svg" fill="#ffffff" height="20" width="20" viewBox="0 0 24 24">
<path d="M23.444,10.239a22.936,22.936,0,0,0-2.492-2.948l-4.021,4.021A5.026,5.026,0,0,1,17,12a5,5,0,0,1-5,5,5.026,5.026,0,0,1-.688-.069L8.055,20.188A10.286,10.286,0,0,0,12,21c5.708,0,9.905-5.062,11.445-7.24A3.058,3.058,0,0,0,23.444,10.239Z"></path>
<path d="M12,3C6.292,3,2.1,8.062,.555,10.24a3.058,3.058,0,0,0,0,3.52h0a21.272,21.272,0,0,0,4.784,4.9l3.124-3.124a5,5,0,0,1,7.071-7.072L8.464,15.536l10.2-10.2A11.484,11.484,0,0,0,12,3Z"></path>
<path data-color="color-2" d="M1,24a1,1,0,0,1-.707-1.707l22-22a1,1,0,0,1,1.414,1.414l-22,22A1,1,0,0,1,1,24Z"></path>
</svg>
</button>
</div>
<div class="flex items-baseline mt-2">
<div id="total-usd-value" class="text-5xl font-bold text-white"></div>
<div id="usd-text" class="text-sm text-white ml-1">USD</div>
</div>
<div id="total-btc-value" class="text-sm text-white mt-2"></div>
<div class="flex items-center">
<h2 class="text-lg font-bold text-white tracking-tighter mr-2">Total Assets:</h2>
<button id="hide-usd-amount-toggle" class="flex items-center justify-center p-1 focus:ring-0 focus:outline-none">{{ eye_show_svg | safe }}</button>
</div>
<div class="flex items-baseline mt-2">
<div id="total-usd-value" class="text-5xl font-bold text-white"></div>
<div id="usd-text" class="text-sm text-white ml-1">USD</div>
</div>
<div id="total-btc-value" class="text-sm text-white mt-2"></div>
</div>
<div class="w-full md:w-1/2 p-3 p-6 container flex flex-wrap items-center justify-end items-center mx-auto">
<a class="mr-5 flex flex-wrap justify-center px-5 py-3 bg-blue-500 hover:bg-blue-600 font-medium text-sm text-white border dark:bg-gray-500 dark:hover:bg-gray-700 border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none" id="refresh" href="/changepassword">
<svg class="text-gray-500 w-5 h-5 mr-2" xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 24 24">
<g stroke-linecap="round" stroke-width="2" fill="none" stroke="#ffffff" stroke-linejoin="round">
<rect x="3" y="11" width="18" height="12" rx="2"></rect>
<circle cx="12" cy="17" r="2" stroke="#ffffff"></circle>
<path d="M17,7V6a4.951,4.951,0,0,0-4.9-5H12A4.951,4.951,0,0,0,7,5.9V7" stroke="#ffffff"></path>
</g>
</svg>
<span>Change Password</span>
</a>
<a class="flex flex-wrap justify-center px-5 py-3 bg-blue-500 hover:bg-blue-600 font-medium text-sm text-white border dark:bg-gray-500 dark:hover:bg-gray-700 border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none" id="refresh" href="/wallets">
<svg class="text-gray-500 w-5 h-5 mr-2" xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 24 24">
<g fill="#ffffff" class="nc-icon-wrapper">
<path fill="#ffffff" d="M12,3c1.989,0,3.873,0.65,5.43,1.833l-3.604,3.393l9.167,0.983L22.562,0l-3.655,3.442 C16.957,1.862,14.545,1,12,1C5.935,1,1,5.935,1,12h2C3,7.037,7.037,3,12,3z"></path>
<path data-color="color-2" d="M12,21c-1.989,0-3.873-0.65-5.43-1.833l3.604-3.393l-9.167-0.983L1.438,24l3.655-3.442 C7.043,22.138,9.455,23,12,23c6.065,0,11-4.935,11-11h-2C21,16.963,16.963,21,12,21z"></path>
</g>
</svg>
<span>Refresh</span>
</a>
<div class="w-full md:w-1/2 p-3 p-6 container flex flex-wrap items-center justify-end items-center mx-auto">
<a class="rounded-full mr-5 flex flex-wrap justify-center px-5 py-3 bg-blue-500 hover:bg-blue-600 font-medium text-sm text-white border dark:bg-gray-500 dark:hover:bg-gray-700 border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none" id="refresh" href="/changepassword">{{ lock_svg | safe }}<span>Change Password</span></a>
<a class="rounded-full flex flex-wrap justify-center px-5 py-3 bg-blue-500 hover:bg-blue-600 font-medium text-sm text-white border dark:bg-gray-500 dark:hover:bg-gray-700 border-blue-500 rounded-md shadow-button focus:ring-0 focus:outline-none" id="refresh" href="/wallets">{{ circular_arrows_svg | safe }}<span>Refresh</span></a>
</div>
</div>
</div>
</div>
</section>
{% include 'inc_messages.html' %}
<section class="py-4">
<div class="container px-4 mx-auto">
<div class="flex flex-wrap -m-4"> {% for w in wallets %} {% if w.havedata %} {% if w.error %} <p>Error: {{ w.error }}</p> {% else %} <div class="w-full lg:w-1/3 p-4">
<div class="flex flex-wrap -m-4">
{% for w in wallets %}
{% if w.havedata %}
{% if w.error %}<p>Error: {{ w.error }}</p>
{% else %}
<div class="w-full lg:w-1/3 p-4">
<div class="bg-gray-50 shadow rounded overflow-hidden dark:bg-gray-500">
<div class="pt-6 px-6 mb-10 flex justify-between items-center">
<span class="inline-flex items-center justify-center w-9 h-10 bg-white-50 rounded">
<img class="h-9" src="/static/images/coins/{{ w.name }}.png" alt="">
<img class="h-9" src="/static/images/coins/{{ w.name }}.png" alt="{{ w.name }}">
</span>
<a class="py-2 px-3 bg-blue-500 text-xs text-white rounded-full hover:bg-blue-600" href="/wallet/{{ w.ticker }}">Manage</a>
<a class="py-2 px-3 bg-blue-500 text-xs text-white rounded-full hover:bg-blue-600" href="/wallet/{{ w.ticker }}">Manage Wallet</a>
</div>
<div class="px-6 mb-6">
<h4 class="text-xl font-bold dark:text-white">{{ w.name }}
<span class="inline-block font-medium text-xs text-gray-500 dark:text-white">({{ w.ticker }})</span>
</h4>
<p class="text-xs text-gray-500 dark:text-gray-200">Version: {{ w.version }} {% if w.updating %} <span class="inline-block py-1 px-2 rounded-full bg-blue-100 text-xs text-black-500 dark:bg-gray-700 dark:hover:bg-gray-700">Updating..</span>
</p>{% endif %}
<p class="text-xs text-gray-500 dark:text-gray-200">Version: {{ w.version }} {% if w.updating %} <span class="inline-block py-1 px-2 rounded-full bg-blue-100 text-xs text-black-500 dark:bg-gray-700 dark:hover:bg-gray-700">Updating..</span></p>
{% endif %}
</div>
<div class="p-6 bg-coolGray-100 dark:bg-gray-600">
<div class="flex mb-2 justify-between items-center">
@@ -103,17 +76,17 @@
<h4 class="text-xs font-medium dark:text-white">{{ w.ticker }} USD value:</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 usd-value"></div>
</div>
{% if w.unconfirmed %}
{% if w.pending %}
<div class="flex mb-2 justify-between items-center">
<h4 class="text-xs font-bold text-green-500 dark:text-green-500">Unconfirmed:</h4>
<span class="bold inline-block py-1 px-2 rounded-full bg-green-100 text-xs text-green-500 dark:bg-gray-500 dark:text-green-500 coinname-value" data-coinname="{{ w.name }}">+{{ w.unconfirmed }} {{ w.ticker }}</span>
<h4 class="text-xs font-bold text-green-500 dark:text-green-500">Pending:</h4>
<span class="bold inline-block py-1 px-2 rounded-full bg-green-100 text-xs text-green-500 dark:bg-gray-500 dark:text-green-500 coinname-value" data-coinname="{{ w.name }}">+{{ w.pending }} {{ w.ticker }}</span>
</div>
<div class="flex mb-2 justify-between items-center">
<h4 class="text-xs font-bold text-green-500 dark:text-green-500">Unconfirmed USD value:</h4>
<h4 class="text-xs font-bold text-green-500 dark:text-green-500">Pending USD value:</h4>
<div class="bold inline-block py-1 px-2 rounded-full bg-green-100 text-xs text-green-500 dark:bg-gray-500 dark:text-green-500 usd-value"></div>
</div>
{% endif %}
{% if w.cid == '1' %}
{% 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>
@@ -130,7 +103,7 @@
<div class="flex mb-2 justify-between items-center">
<h4 class="text-xs font-bold text-green-500 dark:text-green-500">Blind Unconfirmed USD value:</h4>
<div class="bold inline-block py-1 px-2 rounded-full bg-green-100 text-xs text-green-500 dark:bg-gray-500 dark:text-green-500 usd-value"></div>
</div>
</div>
{% endif %}
<div class="flex mb-2 justify-between items-center">
<h4 class="text-xs font-medium dark:text-white">Anon Balance:</h4>
@@ -149,32 +122,61 @@
<div class="flex mb-2 justify-between items-center">
<h4 class="text-xs font-bold text-green-500 dark:text-green-500">Anon Pending USD value:</h4>
<div class="bold inline-block py-1 px-2 rounded-full bg-green-100 text-xs text-green-500 dark:bg-gray-500 dark:text-green-500 usd-value"></div>
</div>
</div>
{% endif %}
{% endif %} {# / PART #}
{% if w.cid == '3' %} {# LTC #}
<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>
</div>
<div class="flex mb-2 justify-between items-center">
<h4 class="text-xs font-medium dark:text-white">MWEB USD value:</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 usd-value"></div>
</div>
{% if w.mweb_pending %}
<div class="flex mb-2 justify-between items-center">
<h4 class="text-xs font-bold text-green-500 dark:text-green-500">MWEB Pending:</h4>
<span class="bold inline-block py-1 px-2 rounded-full bg-green-100 text-xs text-green-500 dark:bg-gray-500 dark:text-green-500 coinname-value" data-coinname="{{ w.name }}">
+{{ w.mweb_pending }} {{ w.ticker }}</span>
</div>
<div class="flex mb-2 justify-between items-center">
<h4 class="text-xs font-bold text-green-500 dark:text-green-500">MWEB Pending USD value:</h4>
<div class="bold inline-block py-1 px-2 rounded-full bg-green-100 text-xs text-green-500 dark:bg-gray-500 dark:text-green-500 usd-value"></div>
</div>
{% endif %}
{% endif %}
{# / LTC #}
<hr class="border-t border-gray-100 dark:border-gray-500 my-5">
<div class="flex mb-2 justify-between items-center">
<h4 class="text-xs font-medium dark:text-white">Blocks:</h4>
<span class="inline-block py-1 px-2 rounded-full bg-blue-100 text-xs text-black-500 dark:bg-gray-500 dark:text-gray-200">{{ w.blocks }}{% if w.known_block_count %} / {{ w.known_block_count }}{% endif %}</span>
<span class="inline-block py-1 px-2 rounded-full bg-blue-100 text-xs text-black-500 dark:bg-gray-500 dark:text-gray-200">{{ w.blocks }}{% if w.known_block_count %} / {{ w.known_block_count }}
{% endif %}
</span>
</div>
<div class="flex mb-2 justify-between items-center">
<h4 class="text-xs font-medium dark:text-white">Last Updated:</h4>
<span class="inline-block py-1 px-2 rounded-full bg-blue-100 text-xs text-black-500 dark:bg-gray-500 dark:text-gray-200">{{ w.lastupdated }}</span>
</div>{% if w.bootstrapping %}
</div>
{% if w.bootstrapping %}
<div class="flex mb-2 justify-between items-center">
<h4 class="text-xs font-medium dark:text-white">Bootstrapping:</h4>
<span class="inline-block py-1 px-2 rounded-full bg-blue-100 text-xs text-black-500 dark:bg-gray-500 dark:text-gray-200">{{ w.bootstrapping }}</span>
</div>{% endif %}
</div>
{% endif %}
{% if w.encrypted %}
<div class="flex mb-2 justify-between items-center">
<h4 class="text-xs font-medium dark:text-white">Locked:</h4>
<span class="inline-block py-1 px-2 rounded-full bg-blue-100 text-xs text-black-500 dark:bg-gray-500 dark:text-gray-200">{{ w.locked }}</span>
</div>{% endif %} <div class="flex mb-2 justify-between items-center">
</div>
{% endif %}
<div class="flex mb-2 justify-between items-center">
<h4 class="text-xs font-medium dark:text-white">Expected Seed:</h4>
<span class="inline-block py-1 px-2 rounded-full bg-blue-100 text-xs text-black-500 dark:bg-gray-500 dark:text-gray-200">{{ w.expected_seed }}</span>
</div>
<div class="flex justify-between mb-1 mt-10">
<span class="text-xs font-medium text-blue-700 dark:text-gray-200">Blockchain</span>
<span class="text-xs font-medium text-blue-700 dark:text-gray-200">{{ w.synced }}%</span>
<span class="text-xs font-medium dark:text-gray-200">Blockchain</span>
<span class="text-xs font-medium dark:text-gray-200">{{ w.synced }}%</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-1">
<div class="bg-blue-500 h-1 rounded-full" style="width: {{ w.synced }}%"></div>
@@ -183,12 +185,14 @@
</div>
{% endif %}
{% endif %}
<!-- havedata -->
</div> {% endfor %}
</div>
{% endfor %}
</div>
</section>
</div>
{% include 'footer.html' %}
<script>
const coinNameToSymbol = {
'Bitcoin': 'BTC',
@@ -409,4 +413,4 @@ window.onload = async () => {
};
</script>
</body>
</html>
</html>

View File

@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2022-2023 tecnovert
# Copyright (c) 2022-2024 tecnovert
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -10,6 +10,7 @@ from .util import (
get_data_entry,
have_data_entry,
get_data_entry_or,
listBidActions,
listBidStates,
listOldBidStates,
set_pagination_filters,
@@ -73,6 +74,7 @@ def page_bid(self, url_split, post_string):
elif b'edit_bid_submit' in form_data:
data = {
'bid_state': int(form_data[b'new_state'][0]),
'bid_action': int(get_data_entry_or(form_data, 'new_action', -1)),
'debug_ind': int(get_data_entry_or(form_data, 'debugind', -1)),
'kbs_other': get_data_entry_or(form_data, 'kbs_other', None),
}
@@ -107,6 +109,14 @@ def page_bid(self, url_split, post_string):
if len(data['addr_from_label']) > 0:
data['addr_from_label'] = '(' + data['addr_from_label'] + ')'
page_data = {
'bid_states': listBidStates(),
'bid_actions': []
}
if swap_client.debug_ui:
data['bid_actions'] = [(-1, 'None'), ] + listBidActions()
template = server.env.get_template('bid_xmr.html' if offer.swap_type == SwapTypes.XMR_SWAP else 'bid.html')
return self.render_template(template, {
'bid_id': bid_id.hex(),
@@ -175,9 +185,8 @@ def page_bids(self, url_split, post_string, sent=False, available=False, receive
bids = swap_client.listBids(sent=sent, filters=filters)
page_data = {
'bid_states': listBidStates()
'bid_states': listBidStates(),
}
template = server.env.get_template('bids.html')
return self.render_template(template, {
'page_type_sent': 'Bids Sent' if sent else '',

View File

@@ -61,6 +61,8 @@ def page_unlock(self, url_split, post_string):
self.end_headers()
return bytes()
except Exception as e:
if swap_client.debug is True:
swap_client.log.error(str(e))
err_messages.append(str(e))
template = server.env.get_template('unlock.html')

View File

@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2022-2023 tecnovert
# Copyright (c) 2022-2024 tecnovert
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -41,6 +41,9 @@ from basicswap.chainparams import (
)
default_chart_api_key = '95dd900af910656e0e17c41f2ddc5dba77d01bf8b0e7d2787634a16bd976c553'
def value_or_none(v):
if v == -1 or v == '-1':
return None
@@ -170,7 +173,7 @@ def parseOfferFormData(swap_client, form_data, page_data, options={}):
try:
swap_client.validateSwapType(coin_from, coin_to, swap_type)
except Exception as e:
errors.append(f'Invalid Swap type {e}')
errors.append(f'{e}')
if have_data_entry(form_data, 'step1'):
if len(errors) == 0 and have_data_entry(form_data, 'continue'):
@@ -217,7 +220,7 @@ def parseOfferFormData(swap_client, form_data, page_data, options={}):
try:
if len(errors) == 0 and page_data['swap_style'] == 'xmr':
reverse_bid: bool = coin_from in swap_client.scriptless_coins
reverse_bid: bool = swap_client.is_reverse_ads_bid(coin_from)
ci_leader = ci_to if reverse_bid else ci_from
ci_follower = ci_from if reverse_bid else ci_to
@@ -261,10 +264,14 @@ def postNewOfferFromParsed(swap_client, parsed_data):
elif parsed_data['coin_to'] in (Coins.XMR, Coins.PART_ANON):
swap_type = SwapTypes.XMR_SWAP
if swap_client.coin_clients[parsed_data['coin_from']]['use_csv'] and swap_client.coin_clients[parsed_data['coin_to']]['use_csv']:
if swap_type == SwapTypes.XMR_SWAP:
# All coins capable of segwit should be capable of csv
lock_type = TxLockTypes.SEQUENCE_LOCK_TIME
else:
lock_type = TxLockTypes.ABS_LOCK_TIME
if swap_client.coin_clients[parsed_data['coin_from']]['use_csv'] and swap_client.coin_clients[parsed_data['coin_to']]['use_csv']:
lock_type = TxLockTypes.SEQUENCE_LOCK_TIME
else:
lock_type = TxLockTypes.ABS_LOCK_TIME
extra_options = {}
@@ -441,7 +448,7 @@ def page_newoffer(self, url_split, post_string):
chart_api_key = swap_client.settings.get('chart_api_key', '')
if chart_api_key == '':
chart_api_key_enc = swap_client.settings.get('chart_api_key_enc', '')
chart_api_key = 'cd7600e7b5fdd99c6f900673ff0ee8f64d6d4219a4bb87191ad4a2e3fc65d7f4' if chart_api_key_enc == '' else bytes.fromhex(chart_api_key_enc).decode('utf-8')
chart_api_key = default_chart_api_key if chart_api_key_enc == '' else bytes.fromhex(chart_api_key_enc).decode('utf-8')
return self.render_template(template, {
'messages': messages,
@@ -484,7 +491,7 @@ def page_offer(self, url_split, post_string):
ci_from = swap_client.ci(Coins(offer.coin_from))
ci_to = swap_client.ci(Coins(offer.coin_to))
reverse_bid: bool = Coins(offer.coin_from) in swap_client.scriptless_coins
reverse_bid: bool = True if offer.bid_reversed else False
# Set defaults
debugind = -1
@@ -586,7 +593,7 @@ def page_offer(self, url_split, post_string):
'is_expired': offer.expire_at <= now,
'active_ind': offer.active_ind,
'swap_type': strSwapDesc(offer.swap_type),
'reverse': offer.bid_reversed
'reverse': reverse_bid,
}
data.update(extend_data)
@@ -742,7 +749,7 @@ def page_offers(self, url_split, post_string, sent=False):
chart_api_key = swap_client.settings.get('chart_api_key', '')
if chart_api_key == '':
chart_api_key_enc = swap_client.settings.get('chart_api_key_enc', '')
chart_api_key = 'cd7600e7b5fdd99c6f900673ff0ee8f64d6d4219a4bb87191ad4a2e3fc65d7f4' if chart_api_key_enc == '' else bytes.fromhex(chart_api_key_enc).decode('utf-8')
chart_api_key = default_chart_api_key if chart_api_key_enc == '' else bytes.fromhex(chart_api_key_enc).decode('utf-8')
template = server.env.get_template('offers.html')
return self.render_template(template, {

View File

@@ -60,7 +60,7 @@ def page_settings(self, url_split, post_string):
data['manage_daemon'] = True if get_data_entry(form_data, 'managedaemon_' + name) == 'true' else False
data['rpchost'] = get_data_entry(form_data, 'rpchost_' + name)
data['rpcport'] = int(get_data_entry(form_data, 'rpcport_' + name))
data['remotedaemonurls'] = get_data_entry(form_data, 'remotedaemonurls_' + name)
data['remotedaemonurls'] = get_data_entry_or(form_data, 'remotedaemonurls_' + name, '')
data['automatically_select_daemon'] = True if get_data_entry(form_data, 'autosetdaemon_' + name) == 'true' else False
else:
data['conf_target'] = int(get_data_entry(form_data, 'conf_target_' + name))

View File

@@ -41,22 +41,34 @@ def format_wallet_data(swap_client, ci, w):
wf['bootstrapping'] = True
if 'known_block_count' in w:
wf['known_block_count'] = w['known_block_count']
if 'locked_utxos' in w:
wf['locked_utxos'] = w['locked_utxos']
if 'balance' in w and 'unconfirmed' in w:
wf['balance_all'] = float(w['balance']) + float(w['unconfirmed'])
if 'lastupdated' in w:
wf['lastupdated'] = format_timestamp(w['lastupdated'])
pending: int = 0
if 'unconfirmed' in w and float(w['unconfirmed']) > 0.0:
wf['unconfirmed'] = w['unconfirmed']
pending += ci.make_int(w['unconfirmed'])
if 'immature' in w and float(w['immature']) > 0.0:
pending += ci.make_int(w['immature'])
if pending > 0.0:
wf['pending'] = ci.format_amount(pending)
if ci.coin_type() == Coins.PART:
wf['stealth_address'] = w.get('stealth_address', '?')
wf['blind_balance'] = "{:.8f}".format(float(w['blind_balance']))
wf['blind_balance'] = w.get('blind_balance', '?')
if 'blind_unconfirmed' in w and float(w['blind_unconfirmed']) > 0.0:
wf['blind_unconfirmed'] = w['blind_unconfirmed']
wf['anon_balance'] = w.get('anon_balance', '?')
if 'anon_pending' in w and float(w['anon_pending']) > 0.0:
wf['anon_pending'] = w['anon_pending']
elif ci.coin_type() == Coins.LTC:
wf['mweb_address'] = w.get('mweb_address', '?')
wf['mweb_balance'] = w.get('mweb_balance', '?')
wf['mweb_pending'] = w.get('mweb_pending', '?')
checkAddressesOwned(swap_client, ci, wf)
return wf
@@ -128,6 +140,8 @@ def page_wallet(self, url_split, post_string):
if bytes('newaddr_' + cid, 'utf-8') in form_data:
swap_client.cacheNewAddressForCoin(coin_id)
elif bytes('newmwebaddr_' + cid, 'utf-8') in form_data:
swap_client.cacheNewStealthAddressForCoin(coin_id)
elif bytes('reseed_' + cid, 'utf-8') in form_data:
try:
swap_client.reseedWallet(coin_id)
@@ -158,22 +172,28 @@ def page_wallet(self, url_split, post_string):
page_data['wd_type_to_' + cid] = type_to
except Exception as e:
err_messages.append('Missing type')
elif coin_id == Coins.LTC:
try:
type_from = form_data[bytes('withdraw_type_from_' + cid, 'utf-8')][0].decode('utf-8')
page_data['wd_type_from_' + cid] = type_from
except Exception as e:
err_messages.append('Missing type')
if len(messages) == 0:
ci = swap_client.ci(coin_id)
ticker = ci.ticker()
if coin_id == Coins.PART:
try:
try:
if coin_id == Coins.PART:
txid = swap_client.withdrawParticl(type_from, type_to, value, address, subfee)
messages.append('Withdrew {} {} ({} to {}) to address {}<br/>In txid: {}'.format(value, ticker, type_from, type_to, address, txid))
except Exception as e:
err_messages.append(str(e))
else:
try:
elif coin_id == Coins.LTC:
txid = swap_client.withdrawLTC(type_from, value, address, subfee)
messages.append('Withdrew {} {} (from {}) to address {}<br/>In txid: {}'.format(value, ticker, type_from, address, txid))
else:
txid = swap_client.withdrawCoin(coin_id, value, address, subfee)
messages.append('Withdrew {} {} to address {}<br/>In txid: {}'.format(value, ticker, address, txid))
except Exception as e:
err_messages.append(str(e))
except Exception as e:
err_messages.append(str(e))
swap_client.updateWalletsInfo(True, coin_id)
elif have_data_entry(form_data, 'showutxogroups'):
show_utxo_groups = True
@@ -227,6 +247,8 @@ def page_wallet(self, url_split, post_string):
if k == Coins.XMR:
wallet_data['main_address'] = w.get('main_address', 'Refresh necessary')
elif k == Coins.LTC:
wallet_data['mweb_address'] = w.get('mweb_address', 'Refresh necessary')
if 'wd_type_from_' + cid in page_data:
wallet_data['wd_type_from'] = page_data['wd_type_from_' + cid]

View File

@@ -16,16 +16,17 @@ from basicswap.chainparams import (
chainparams,
)
from basicswap.basicswap_util import (
TxTypes,
TxStates,
ActionTypes,
BidStates,
SwapTypes,
strTxType,
DebugTypes,
strTxState,
strBidState,
TxLockTypes,
getLastBidState,
strBidState,
strTxState,
strTxType,
SwapTypes,
TxLockTypes,
TxStates,
TxTypes,
)
from basicswap.protocols.xmr_swap_1 import getChainBSplitKey, getChainBRemoteSplitKey
@@ -145,11 +146,18 @@ def listBidStates():
return rv
def listBidActions():
rv = []
for a in ActionTypes:
rv.append((int(a), a.name))
return rv
def describeBid(swap_client, bid, xmr_swap, offer, xmr_offer, bid_events, edit_bid, show_txns, view_tx_ind=None, for_api=False, show_lock_transfers=False):
ci_from = swap_client.ci(Coins(offer.coin_from))
ci_to = swap_client.ci(Coins(offer.coin_to))
reverse_bid: bool = Coins(offer.coin_from) in swap_client.scriptless_coins
reverse_bid: bool = swap_client.is_reverse_ads_bid(offer.coin_from)
ci_leader = ci_to if reverse_bid else ci_from
ci_follower = ci_from if reverse_bid else ci_to
@@ -419,6 +427,8 @@ def getCoinName(c):
return chainparams[Coins.PART]['name'].capitalize() + ' Anon'
if c == Coins.PART_BLIND:
return chainparams[Coins.PART]['name'].capitalize() + ' Blind'
if c == Coins.LTC_MWEB:
return chainparams[Coins.LTC]['name'].capitalize() + ' MWEB'
coin_chainparams = chainparams[c]
if coin_chainparams.get('use_ticker_as_name', False):
@@ -441,6 +451,9 @@ def listAvailableCoins(swap_client, with_variants=True, split_from=False):
coins.append((int(v), getCoinName(v)))
if split_from and v not in invalid_coins_from:
coins_from.append(coins[-1])
if with_variants and k == Coins.LTC:
for v in (Coins.LTC_MWEB, ):
pass # Add when swappable
if split_from:
return coins_from, coins
return coins

View File

@@ -181,7 +181,7 @@ def format_timestamp(value: int, with_seconds: bool = False) -> str:
str_format = '%Y-%m-%d %H:%M'
if with_seconds:
str_format += ':%S'
str_format += ' %Z'
str_format += ' %z'
return time.strftime(str_format, time.localtime(value))

View File

@@ -1,13 +1,23 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2022 tecnovert
# Copyright (c) 2022-2023 tecnovert
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
from Crypto.Hash import RIPEMD160 # pycryptodome
from Crypto.Hash import RIPEMD160, SHA256 # pycryptodome
def sha256(data):
h = SHA256.new()
h.update(data)
return h.digest()
def ripemd160(data):
h = RIPEMD160.new()
h.update(data)
return h.digest()
def hash160(s):
return ripemd160(sha256(s))

17
basicswap/util/network.py Normal file
View File

@@ -0,0 +1,17 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2024 tecnovert
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import ipaddress
def is_private_ip_address(addr: str):
# Will return false for all URLs
if addr == 'localhost':
return True
try:
return ipaddress.ip_address(addr).is_private
except Exception:
return False

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright (c) 2019-2023 tecnovert
# Copyright (c) 2019-2024 tecnovert
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -38,37 +38,40 @@ from basicswap.util.rfc2440 import rfc2440_hash_password
from basicswap.contrib.rpcauth import generate_salt, password_to_hmac
from bin.basicswap_run import startDaemon, startXmrWalletDaemon
PARTICL_VERSION = os.getenv('PARTICL_VERSION', '23.1.5.0')
PARTICL_VERSION = os.getenv('PARTICL_VERSION', '23.2.7.0')
PARTICL_VERSION_TAG = os.getenv('PARTICL_VERSION_TAG', '')
PARTICL_LINUX_EXTRA = os.getenv('PARTICL_LINUX_EXTRA', 'nousb')
LITECOIN_VERSION = os.getenv('LITECOIN_VERSION', '0.21.2')
LITECOIN_VERSION = os.getenv('LITECOIN_VERSION', '0.21.2.2')
LITECOIN_VERSION_TAG = os.getenv('LITECOIN_VERSION_TAG', '')
BITCOIN_VERSION = os.getenv('BITCOIN_VERSION', '23.0')
BITCOIN_VERSION = os.getenv('BITCOIN_VERSION', '26.0')
BITCOIN_VERSION_TAG = os.getenv('BITCOIN_VERSION_TAG', '')
MONERO_VERSION = os.getenv('MONERO_VERSION', '0.18.2.2')
MONERO_VERSION = os.getenv('MONERO_VERSION', '0.18.3.1')
MONERO_VERSION_TAG = os.getenv('MONERO_VERSION_TAG', '')
XMR_SITE_COMMIT = 'a3b195eb90c7d5564cc9d9ec09c873783d21901b' # Lock hashes.txt to monero version
XMR_SITE_COMMIT = '1bdb0d456943a224a4d6241b1bb713172e5fa29f' # Lock hashes.txt to monero version
PIVX_VERSION = os.getenv('PIVX_VERSION', '5.5.0')
PIVX_VERSION_TAG = os.getenv('PIVX_VERSION_TAG', '')
DASH_VERSION = os.getenv('DASH_VERSION', '19.3.0')
DASH_VERSION = os.getenv('DASH_VERSION', '20.0.2')
DASH_VERSION_TAG = os.getenv('DASH_VERSION_TAG', '')
FIRO_VERSION = os.getenv('FIRO_VERSION', '0.14.99.1')
FIRO_VERSION = os.getenv('FIRO_VERSION', '0.14.13.1')
FIRO_VERSION_TAG = os.getenv('FIRO_VERSION_TAG', '')
NAV_VERSION = os.getenv('NAV_VERSION', '7.0.3')
NAV_VERSION_TAG = os.getenv('NAV_VERSION', '')
NAV_VERSION_TAG = os.getenv('NAV_VERSION_TAG', '')
GUIX_SSL_CERT_DIR = None
ADD_PUBKEY_URL = os.getenv('ADD_PUBKEY_URL', '')
OVERRIDE_DISABLED_COINS = toBool(os.getenv('OVERRIDE_DISABLED_COINS', 'false'))
# If SKIP_GPG_VALIDATION is set to true the script will check hashes but not signatures
SKIP_GPG_VALIDATION = toBool(os.getenv('SKIP_GPG_VALIDATION', 'false'))
known_coins = {
'particl': (PARTICL_VERSION, PARTICL_VERSION_TAG, ('tecnovert',)),
@@ -78,8 +81,7 @@ known_coins = {
'monero': (MONERO_VERSION, MONERO_VERSION_TAG, ('binaryfate',)),
'pivx': (PIVX_VERSION, PIVX_VERSION_TAG, ('fuzzbawls',)),
'dash': (DASH_VERSION, DASH_VERSION_TAG, ('pasta',)),
# 'firo': (FIRO_VERSION, FIRO_VERSION_TAG, ('reuben',)),
'firo': (FIRO_VERSION, FIRO_VERSION_TAG, ('tecnovert',)),
'firo': (FIRO_VERSION, FIRO_VERSION_TAG, ('reuben',)),
'navcoin': (NAV_VERSION, NAV_VERSION_TAG, ('nav_builder',)),
}
@@ -108,9 +110,17 @@ elif USE_PLATFORM == 'Windows':
BIN_ARCH = 'win64'
FILE_EXT = 'zip'
else:
BIN_ARCH = 'x86_64-linux-gnu'
machine: str = platform.machine()
if 'arm' in machine:
BIN_ARCH = 'arm-linux-gnueabihf'
else:
BIN_ARCH = machine + '-linux-gnu'
FILE_EXT = 'tar.gz'
# Allow manually overriding the arch tag
BIN_ARCH = os.getenv('BIN_ARCH', BIN_ARCH)
FILE_EXT = os.getenv('FILE_EXT', FILE_EXT)
logger = logging.getLogger()
logger.level = logging.INFO
if not len(logger.handlers):
@@ -190,13 +200,32 @@ BITCOIN_FASTSYNC_FILE = os.getenv('BITCOIN_FASTSYNC_FILE', 'utxo-snapshot-bitcoi
# Encrypt new wallets with this password, must match the Particl wallet password when adding coins
WALLET_ENCRYPTION_PWD = os.getenv('WALLET_ENCRYPTION_PWD', '')
use_tor_proxy = False
use_tor_proxy: bool = False
monerod_proxy_config = [
f'proxy={TOR_PROXY_HOST}:{TOR_PROXY_PORT}',
'proxy-allow-dns-leaks=0',
'no-igd=1', # Disable UPnP port mapping
'hide-my-port=1', # Don't share the p2p port
'p2p-bind-ip=127.0.0.1', # Don't broadcast ip
'in-peers=0', # Changes "error" in log to "incoming connections disabled"
]
monero_wallet_rpc_proxy_config = [
'daemon-ssl-allow-any-cert=1',
]
default_socket = socket.socket
default_socket_timeout = socket.getdefaulttimeout()
default_socket_getaddrinfo = socket.getaddrinfo
def exitWithError(error_msg):
sys.stderr.write('Error: {}, exiting.\n'.format(error_msg))
sys.exit(1)
def make_reporthook(read_start=0):
read = read_start # Number of bytes read so far
last_percent_str = ''
@@ -312,7 +341,7 @@ def urlretrieve(url, filename, reporthook=None, data=None, resume_from=0):
return result
def setConnectionParameters(timeout=5):
def setConnectionParameters(timeout: int = 5, allow_set_tor: bool = True):
opener = urllib.request.build_opener()
opener.addheaders = [('User-agent', 'Mozilla/5.0')]
urllib.request.install_opener(opener)
@@ -515,6 +544,8 @@ def extractCore(coin, version_data, settings, bin_dir, release_path, extra_opts=
if coin == 'pivx':
filename = '{}-{}/bin/{}'.format(dir_name, version, b)
elif coin == 'particl' and '_nousb-' in release_path:
filename = '{}-{}_nousb/bin/{}'.format(dir_name, version + version_tag, b)
else:
filename = '{}-{}/bin/{}'.format(dir_name, version + version_tag, b)
@@ -553,7 +584,14 @@ def prepareCore(coin, version_data, settings, data_dir, extra_opts={}):
release_filename = '{}-{}-{}.{}'.format(coin, version, BIN_ARCH, use_file_ext)
if os_name == 'osx':
os_name = 'mac'
release_url = 'https://downloads.getmonero.org/cli/monero-{}-x64-v{}.{}'.format(os_name, version, use_file_ext)
architecture = 'x64'
if 'aarch64' in BIN_ARCH:
architecture = 'armv8'
elif 'arm' in BIN_ARCH:
architecture = 'armv7'
release_url = 'https://downloads.getmonero.org/cli/monero-{}-{}-v{}.{}'.format(os_name, architecture, version, use_file_ext)
release_path = os.path.join(bin_dir, release_filename)
if not os.path.exists(release_path):
downloadFile(release_url, release_path)
@@ -566,36 +604,36 @@ def prepareCore(coin, version_data, settings, data_dir, extra_opts={}):
downloadFile(assert_url, assert_path)
else:
major_version = int(version.split('.')[0])
use_guix: bool = coin in ('dash', ) or major_version >= 22
arch_name = BIN_ARCH
if major_version >= 23:
if os_name == 'osx':
arch_name = 'x86_64-apple-darwin'
if coin == 'particl':
arch_name += '18'
if os_name == 'osx' and use_guix:
arch_name = 'x86_64-apple-darwin'
if coin == 'particl':
arch_name += '18'
release_filename = '{}-{}-{}.{}'.format(coin, version + version_tag, arch_name, FILE_EXT)
if filename_extra != '':
if major_version >= 23:
if use_guix:
release_filename = '{}-{}_{}-{}.{}'.format(coin, version + version_tag, filename_extra, arch_name, FILE_EXT)
else:
release_filename = '{}-{}-{}_{}.{}'.format(coin, version + version_tag, arch_name, filename_extra, FILE_EXT)
release_filename = '{}-{}-{}.{}'.format(coin, version + version_tag, arch_name, FILE_EXT)
if coin == 'particl':
release_url = 'https://github.com/particl/particl-core/releases/download/v{}/{}'.format(version + version_tag, release_filename)
assert_filename = '{}-{}-{}-build.assert'.format(coin, os_name, version)
if major_version >= 22:
if use_guix:
assert_url = f'https://raw.githubusercontent.com/particl/guix.sigs/master/{version}/{signing_key_name}/all.SHA256SUMS'
else:
assert_url = 'https://raw.githubusercontent.com/particl/gitian.sigs/master/%s-%s/%s/%s' % (version + version_tag, os_dir_name, signing_key_name, assert_filename)
elif coin == 'litecoin':
release_url = 'https://download.litecoin.org/litecoin-{}/{}/{}'.format(version, os_name, release_filename)
assert_filename = '{}-core-{}-{}-build.assert'.format(coin, os_name, version.rsplit('.', 1)[0])
assert_url = 'https://raw.githubusercontent.com/litecoin-project/gitian.sigs.ltc/master/%s-%s/%s/%s' % (version, os_dir_name, signing_key_name, assert_filename)
release_url = 'https://github.com/litecoin-project/litecoin/releases/download/v{}/{}'.format(version + version_tag, release_filename)
assert_filename = '{}-core-{}-{}-build.assert'.format(coin, os_name, '.'.join(version.split('.')[:2]))
use_signing_key_name = (signing_key_name + '/' + signing_key_name) if os_name == 'win' else signing_key_name
assert_url = 'https://raw.githubusercontent.com/litecoin-project/gitian.sigs.ltc/master/%s-%s/%s/%s' % (version, os_dir_name, use_signing_key_name, assert_filename)
elif coin == 'bitcoin':
release_url = 'https://bitcoincore.org/bin/bitcoin-core-{}/{}'.format(version, release_filename)
assert_filename = '{}-core-{}-{}-build.assert'.format(coin, os_name, '.'.join(version.split('.')[:2]))
if major_version >= 22:
if use_guix:
assert_url = f'https://raw.githubusercontent.com/bitcoin-core/guix.sigs/main/{version}/{signing_key_name}/all.SHA256SUMS'
else:
assert_url = 'https://raw.githubusercontent.com/bitcoin-core/gitian.sigs/master/%s-%s/%s/%s' % (version, os_dir_name, signing_key_name, assert_filename)
@@ -609,33 +647,19 @@ def prepareCore(coin, version_data, settings, data_dir, extra_opts={}):
assert_filename = '{}-{}-{}-build.assert'.format(coin, os_name, version.rsplit('.', 1)[0])
assert_url = 'https://raw.githubusercontent.com/PIVX-Project/gitian.sigs/master/%s-%s/%s/%s' % (version + version_tag, os_dir_name, signing_key_name.capitalize(), assert_filename)
elif coin == 'dash':
release_filename = '{}-{}-{}.{}'.format('dashcore', version + version_tag, BIN_ARCH, FILE_EXT)
release_filename = '{}-{}-{}.{}'.format('dashcore', version + version_tag, arch_name, FILE_EXT)
release_url = 'https://github.com/dashpay/dash/releases/download/v{}/{}'.format(version + version_tag, release_filename)
assert_filename = '{}-{}-{}-build.assert'.format(coin, os_name, major_version)
assert_url = 'https://raw.githubusercontent.com/dashpay/gitian.sigs/master/%s-%s/%s/%s' % (version + version_tag, os_dir_name, signing_key_name, assert_filename)
assert_filename = '{}-{}-{}-build.assert'.format(coin, arch_name, major_version)
assert_url = f'https://raw.githubusercontent.com/dashpay/guix.sigs/master/{version}/{signing_key_name}/codesigned.SHA256SUMS'
elif coin == 'firo':
'''
arch_name = BIN_ARCH
if BIN_ARCH == 'x86_64-linux-gnu':
arch_name = 'linux64'
file_ext = 'tar.gz'
elif BIN_ARCH == 'osx64':
arch_name = 'macos'
file_ext = 'dmg'
raise ValueError('TODO: Firo - Extract .dmg')
else:
raise ValueError('Firo: Unknown architecture')
release_filename = '{}-{}-{}{}.{}'.format('firo', version + version_tag, arch_name, filename_extra, file_ext)
# release_url = 'https://github.com/firoorg/firo/releases/download/v{}/{}'.format(version + version_tag, release_filename)
# assert_url = 'https://github.com/firoorg/firo/releases/download/v%s/SHA256SUMS' % (version + version_tag)
'''
if BIN_ARCH == 'x86_64-linux-gnu':
release_filename = 'firo-0.14.99.1-x86_64-linux-gnu.tar.gz'
elif BIN_ARCH == 'osx64':
release_filename = 'firo-0.14.99.1-x86_64-apple-darwin18.tar.gz'
else:
raise ValueError('Firo: Unknown architecture')
release_url = 'https://github.com/tecnovert/particl-core/releases/download/{}/{}'.format(version + version_tag, release_filename)
assert_url = 'https://github.com/tecnovert/particl-core/releases/download/v%s/SHA256SUMS.asc' % (version + version_tag)
release_filename = '{}-{}-{}{}.{}'.format('firo', version + version_tag, arch_name, filename_extra, FILE_EXT)
release_url = 'https://github.com/firoorg/firo/releases/download/v{}/{}'.format(version + version_tag, release_filename)
assert_url = 'https://github.com/firoorg/firo/releases/download/v%s/SHA256SUMS' % (version + version_tag)
elif coin == 'navcoin':
release_filename = '{}-{}-{}.{}'.format(coin, version, BIN_ARCH, FILE_EXT)
release_url = 'https://github.com/navcoin/navcoin-core/releases/download/{}/{}'.format(version + version_tag, release_filename)
@@ -656,7 +680,7 @@ def prepareCore(coin, version_data, settings, data_dir, extra_opts={}):
downloadFile(assert_url, assert_path)
if coin not in ('firo', ):
assert_sig_url = assert_url + ('.asc' if major_version >= 22 else '.sig')
assert_sig_url = assert_url + ('.asc' if use_guix else '.sig')
if coin not in ('nav', ):
assert_sig_filename = '{}-{}-{}-build-{}.assert.sig'.format(coin, os_name, version, signing_key_name)
assert_sig_path = os.path.join(bin_dir, assert_sig_filename)
@@ -675,6 +699,11 @@ def prepareCore(coin, version_data, settings, data_dir, extra_opts={}):
else:
logger.info('Found release hash in assert file.')
if SKIP_GPG_VALIDATION:
logger.warning('Skipping binary signature check as SKIP_GPG_VALIDATION env var is set.')
extractCore(coin, version_data, settings, bin_dir, release_path, extra_opts)
return
"""
gnupghome = os.path.join(data_dir, 'gpg')
if not os.path.exists(gnupghome):
@@ -692,9 +721,7 @@ def prepareCore(coin, version_data, settings, data_dir, extra_opts={}):
for key in rv.fingerprints:
gpg.trust_keys(rv.fingerprints[0], 'TRUST_FULLY')
if coin in ('firo', ):
pubkey_filename = '{}_{}.pgp'.format('particl', signing_key_name)
elif coin in ('navcoin', ):
if coin in ('navcoin', ):
pubkey_filename = '{}_builder.pgp'.format(coin)
else:
pubkey_filename = '{}_{}.pgp'.format(coin, signing_key_name)
@@ -801,9 +828,8 @@ def prepareDataDir(coin, settings, chain, particl_mnemonic, extra_opts={}):
fp.write('prune-blockchain=1\n')
if tor_control_password is not None:
fp.write(f'proxy={TOR_PROXY_HOST}:{TOR_PROXY_PORT}\n')
fp.write('proxy-allow-dns-leaks=0\n')
fp.write('no-igd=1\n')
for opt_line in monerod_proxy_config:
fp.write(opt_line + '\n')
if XMR_RPC_USER != '':
fp.write(f'rpc-login={XMR_RPC_USER}:{XMR_RPC_PWD}\n')
@@ -820,7 +846,7 @@ def prepareDataDir(coin, settings, chain, particl_mnemonic, extra_opts={}):
if extra_opts.get('use_containers', False) is True:
fp.write('daemon-address={}:{}\n'.format(core_settings['rpchost'], core_settings['rpcport']))
config_datadir = '/data'
fp.write('untrusted-daemon=1\n')
fp.write('no-dns=1\n')
fp.write('rpc-bind-port={}\n'.format(core_settings['walletrpcport']))
fp.write('rpc-bind-ip={}\n'.format(COINS_RPCBIND_IP))
@@ -834,7 +860,8 @@ def prepareDataDir(coin, settings, chain, particl_mnemonic, extra_opts={}):
if tor_control_password is not None:
if not core_settings['manage_daemon']:
fp.write(f'proxy={TOR_PROXY_HOST}:{TOR_PROXY_PORT}\n')
for opt_line in monero_wallet_rpc_proxy_config:
fp.write(opt_line + '\n')
return
core_conf_path = os.path.join(data_dir, coin + '.conf')
@@ -883,6 +910,7 @@ def prepareDataDir(coin, settings, chain, particl_mnemonic, extra_opts={}):
if LTC_RPC_USER != '':
fp.write('rpcauth={}:{}${}\n'.format(LTC_RPC_USER, salt, password_to_hmac(salt, LTC_RPC_PWD)))
elif coin == 'bitcoin':
fp.write('deprecatedrpc=create_bdb\n')
fp.write('prune=2000\n')
fp.write('fallbackfee=0.0002\n')
if BTC_RPC_USER != '':
@@ -959,7 +987,7 @@ def addTorSettings(settings, tor_control_password):
settings['tor_control_port'] = TOR_CONTROL_PORT
def modify_tor_config(settings, coin, tor_control_password=None, enable=False):
def modify_tor_config(settings, coin, tor_control_password=None, enable=False, extra_opts={}):
coin_settings = settings['chainclients'][coin]
data_dir = coin_settings['datadir']
@@ -976,30 +1004,29 @@ def modify_tor_config(settings, coin, tor_control_password=None, enable=False):
shutil.copyfile(core_conf_path, core_conf_path + '.last')
shutil.copyfile(wallet_conf_path, wallet_conf_path + '.last')
daemon_tor_settings = ('proxy=', 'proxy-allow-dns-leaks=', 'no-igd=')
with open(core_conf_path, 'w') as fp:
with open(core_conf_path + '.last') as fp_in:
# Disable tor first
for line in fp_in:
skip_line = False
for setting in daemon_tor_settings:
skip_line: bool = False
for opt_line in monerod_proxy_config:
setting: str = opt_line[0: opt_line.find('=') + 1]
if line.startswith(setting):
skip_line = True
break
if not skip_line:
fp.write(line)
if enable:
fp.write(f'proxy={TOR_PROXY_HOST}:{TOR_PROXY_PORT}\n')
fp.write('proxy-allow-dns-leaks=0\n')
fp.write('no-igd=1\n')
for opt_line in monerod_proxy_config:
fp.write(opt_line + '\n')
wallet_tor_settings = ('proxy=',)
with open(wallet_conf_path, 'w') as fp:
with open(wallet_conf_path + '.last') as fp_in:
# Disable tor first
for line in fp_in:
skip_line = False
for setting in wallet_tor_settings:
for opt_line in monero_wallet_rpc_proxy_config + ['proxy=',]:
setting: str = opt_line[0: opt_line.find('=') + 1]
if line.startswith(setting):
skip_line = True
break
@@ -1007,7 +1034,10 @@ def modify_tor_config(settings, coin, tor_control_password=None, enable=False):
fp.write(line)
if enable:
if not coin_settings['manage_daemon']:
fp.write(f'proxy={TOR_PROXY_HOST}:{TOR_PROXY_PORT}\n')
for opt_line in monero_wallet_rpc_proxy_config:
fp.write(opt_line + '\n')
coin_settings['trusted_daemon'] = extra_opts.get('trust_remote_node', 'auto')
return
config_path = os.path.join(data_dir, coin + '.conf')
@@ -1045,11 +1075,6 @@ def modify_tor_config(settings, coin, tor_control_password=None, enable=False):
writeTorSettings(fp, coin, coin_settings, tor_control_password)
def exitWithError(error_msg):
sys.stderr.write('Error: {}, exiting.\n'.format(error_msg))
sys.exit(1)
def printVersion():
logger.info(f'Basicswap version: {__version__}')
@@ -1080,10 +1105,12 @@ def printHelp():
print('--usecontainers Expect each core to run in a unique container.')
print('--portoffset=n Raise all ports by n.')
print('--htmlhost= Interface to host html server on, default:127.0.0.1.')
print('--wshost= Interface to host websocket server on, disable by setting to "none", default:127.0.0.1.')
print('--wshost= Interface to host websocket server on, disable by setting to "none", default\'s to --htmlhost.')
print('--xmrrestoreheight=n Block height to restore Monero wallet from, default:{}.'.format(DEFAULT_XMR_RESTORE_HEIGHT))
print('--trustremotenode Set trusted-daemon for XMR, defaults to auto: true when daemon rpchost value is a private ip address else false')
print('--noextractover Prevent extracting cores if files exist. Speeds up tests')
print('--usetorproxy Use TOR proxy during setup. Note that some download links may be inaccessible over TOR.')
print('--notorproxy Force usetorproxy off, usetorproxy is automatically set when tor is enabled')
print('--enabletor Setup Basicswap instance to use TOR.')
print('--disabletor Setup Basicswap instance to not use TOR.')
print('--usebtcfastsync Initialise the BTC chain with a snapshot from btcpayserver FastSync.\n'
@@ -1115,12 +1142,12 @@ def test_particl_encryption(data_dir, settings, chain, use_tor_proxy):
swap_client = None
daemons = []
daemon_args = ['-noconnect', '-nodnsseed', '-nofindpeers', '-nostaking']
if not use_tor_proxy:
# Cannot set -bind or -whitebind together with -listen=0
daemon_args.append('-nolisten')
with open(os.path.join(data_dir, 'basicswap.log'), 'a') as fp:
try:
swap_client = BasicSwap(fp, data_dir, settings, chain)
swap_client = BasicSwap(fp, data_dir, settings, chain, transient_instance=True)
if not swap_client.use_tor_proxy:
# Cannot set -bind or -whitebind together with -listen=0
daemon_args.append('-nolisten')
c = Coins.PART
coin_name = 'particl'
coin_settings = settings['chainclients'][coin_name]
@@ -1145,18 +1172,23 @@ def test_particl_encryption(data_dir, settings, chain, use_tor_proxy):
finalise_daemon(d)
def encrypt_wallet(swap_client, coin_type) -> None:
ci = swap_client.ci(coin_type)
ci.changeWalletPassword('', WALLET_ENCRYPTION_PWD)
ci.unlockWallet(WALLET_ENCRYPTION_PWD)
def initialise_wallets(particl_wallet_mnemonic, with_coins, data_dir, settings, chain, use_tor_proxy):
swap_client = None
daemons = []
daemon_args = ['-noconnect', '-nodnsseed']
if not use_tor_proxy:
# Cannot set -bind or -whitebind together with -listen=0
daemon_args.append('-nolisten')
with open(os.path.join(data_dir, 'basicswap.log'), 'a') as fp:
try:
swap_client = BasicSwap(fp, data_dir, settings, chain)
swap_client = BasicSwap(fp, data_dir, settings, chain, transient_instance=True)
if not swap_client.use_tor_proxy:
# Cannot set -bind or -whitebind together with -listen=0
daemon_args.append('-nolisten')
coins_to_create_wallets_for = (Coins.PART, Coins.BTC, Coins.LTC, Coins.DASH)
# Always start Particl, it must be running to initialise a wallet in addcoin mode
# Particl must be loaded first as subsequent coins are initialised from the Particl mnemonic
@@ -1189,16 +1221,18 @@ def initialise_wallets(particl_wallet_mnemonic, with_coins, data_dir, settings,
if len(wallets) < 1:
logger.info('Creating wallet.dat for {}.'.format(getCoinName(c)))
if c == Coins.BTC:
if c in (Coins.BTC, Coins.LTC):
# wallet_name, disable_private_keys, blank, passphrase, avoid_reuse, descriptors
swap_client.callcoinrpc(c, 'createwallet', ['wallet.dat', False, True, '', False, False])
swap_client.callcoinrpc(c, 'createwallet', ['wallet.dat', False, True, WALLET_ENCRYPTION_PWD, False, False])
swap_client.ci(c).unlockWallet(WALLET_ENCRYPTION_PWD)
else:
swap_client.callcoinrpc(c, 'createwallet', ['wallet.dat'])
if WALLET_ENCRYPTION_PWD != '':
encrypt_wallet(swap_client, c)
if WALLET_ENCRYPTION_PWD != '':
ci = swap_client.ci(c)
ci.changeWalletPassword('', WALLET_ENCRYPTION_PWD)
ci.unlockWallet(WALLET_ENCRYPTION_PWD)
if c == Coins.LTC:
password = WALLET_ENCRYPTION_PWD if WALLET_ENCRYPTION_PWD != '' else None
swap_client.ci(Coins.LTC_MWEB).init_wallet(password)
if c == Coins.PART:
if 'particl' in with_coins:
@@ -1295,7 +1329,6 @@ def main():
disable_coin = ''
coins_changed = False
htmlhost = '127.0.0.1'
wshost = '127.0.0.1'
xmr_restore_height = DEFAULT_XMR_RESTORE_HEIGHT
prepare_bin_only = False
no_cores = False
@@ -1329,13 +1362,9 @@ def main():
if name == 'h' or name == 'help':
printHelp()
return 0
if name == 'mainnet':
continue
if name == 'testnet':
chain = 'testnet'
continue
if name == 'regtest':
chain = 'regtest'
if name in ('mainnet', 'testnet', 'regtest'):
chain = name
continue
if name == 'preparebinonly':
prepare_bin_only = True
@@ -1352,6 +1381,9 @@ def main():
if name == 'usetorproxy':
use_tor_proxy = True
continue
if name == 'notorproxy':
extra_opts['no_tor_proxy'] = True
continue
if name == 'enabletor':
enable_tor = True
continue
@@ -1364,6 +1396,9 @@ def main():
if name == 'skipbtcfastsyncchecks':
extra_opts['check_btc_fastsync'] = False
continue
if name == 'trustremotenode':
extra_opts['trust_remote_node'] = True
continue
if name == 'initwalletsonly':
initwalletsonly = True
continue
@@ -1380,13 +1415,13 @@ def main():
if name == 'particl_mnemonic':
particl_wallet_mnemonic = s[1].strip('"')
continue
if name == 'withcoin' or name == 'withcoins':
if name in ('withcoin', 'withcoins'):
for coin in [s.lower() for s in s[1].split(',')]:
ensure_coin_valid(coin)
with_coins.add(coin)
coins_changed = True
continue
if name == 'withoutcoin' or name == 'withoutcoins':
if name in ('withoutcoin', 'withoutcoins'):
for coin in [s.lower() for s in s[1].split(',')]:
ensure_coin_valid(coin, test_disabled=False)
with_coins.discard(coin)
@@ -1405,7 +1440,7 @@ def main():
htmlhost = s[1].strip('"')
continue
if name == 'wshost':
wshost = s[1].strip('"')
extra_opts['wshost'] = s[1].strip('"')
continue
if name == 'xmrrestoreheight':
xmr_restore_height = int(s[1])
@@ -1413,23 +1448,19 @@ def main():
if name == 'keysdirpath':
extra_opts['keysdirpath'] = os.path.expanduser(s[1].strip('"'))
continue
if name == 'trustremotenode':
extra_opts['trust_remote_node'] = toBool(s[1])
continue
exitWithError('Unknown argument {}'.format(v))
setConnectionParameters()
if use_tor_proxy and TEST_TOR_PROXY:
testTorConnection()
if use_tor_proxy and TEST_ONION_LINK:
testOnionLink()
if data_dir is None:
data_dir = os.path.join(os.path.expanduser(cfg.BASICSWAP_DATADIR))
if bin_dir is None:
bin_dir = os.path.join(data_dir, 'bin')
logger.info(f'BasicSwap prepare script {__version__}\n')
logger.info(f'Python version: {platform.python_version()}')
logger.info(f'Data dir: {data_dir}')
logger.info(f'Bin dir: {bin_dir}')
logger.info(f'Chain: {chain}')
@@ -1442,6 +1473,31 @@ def main():
os.makedirs(data_dir)
config_path = os.path.join(data_dir, cfg.CONFIG_FILENAME)
if use_tor_proxy and extra_opts.get('no_tor_proxy', False):
exitWithError('Can\'t use --usetorproxy and --notorproxy together')
# Automatically enable tor for certain commands if it's set in basicswap config
if not (initwalletsonly or enable_tor or disable_tor or disable_coin) and \
not use_tor_proxy and os.path.exists(config_path):
settings = load_config(config_path)
settings_use_tor = settings.get('use_tor', False)
if settings_use_tor:
logger.info('use_tor is set in the config')
if extra_opts.get('no_tor_proxy', False):
use_tor_proxy = False
logger.warning('Not automatically setting --usetorproxy as --notorproxy is set')
else:
use_tor_proxy = True
logger.info('Automatically setting --usetorproxy')
setConnectionParameters(allow_set_tor=False)
if use_tor_proxy and TEST_TOR_PROXY:
testTorConnection()
if use_tor_proxy and TEST_ONION_LINK:
testOnionLink()
should_download_btc_fastsync = False
if extra_opts.get('use_btc_fastsync', False) is True:
if 'bitcoin' in with_coins or add_coin == 'bitcoin':
@@ -1501,6 +1557,7 @@ def main():
'blocks_confirmed': 2,
'conf_target': 2,
'core_version_group': 21,
'min_relay_fee': 0.00001,
'chain_lookups': 'local',
},
'bitcoin': {
@@ -1539,6 +1596,7 @@ def main():
'zmqport': BASE_XMR_ZMQ_PORT + port_offset,
'walletrpcport': BASE_XMR_WALLET_PORT + port_offset,
'rpchost': XMR_RPC_HOST,
'trusted_daemon': extra_opts.get('trust_remote_node', 'auto'),
'walletrpchost': XMR_WALLET_RPC_HOST,
'walletrpcuser': XMR_WALLET_RPC_USER,
'walletrpcpassword': XMR_WALLET_RPC_PWD,
@@ -1546,7 +1604,10 @@ def main():
'datadir': os.getenv('XMR_DATA_DIR', os.path.join(data_dir, 'monero')),
'bindir': os.path.join(bin_dir, 'monero'),
'restore_height': xmr_restore_height,
'blocks_confirmed': 7, # TODO: 10?
'blocks_confirmed': 3,
'rpctimeout': 60,
'walletrpctimeout': 120,
'walletrpctimeoutlong': 600,
},
'pivx': {
'connection_type': 'rpc' if 'pivx' in with_coins else 'none',
@@ -1587,10 +1648,11 @@ def main():
'datadir': os.getenv('FIRO_DATA_DIR', os.path.join(data_dir, 'firo')),
'bindir': os.path.join(bin_dir, 'firo'),
'use_segwit': False,
'use_csv': True,
'use_csv': False,
'blocks_confirmed': 1,
'conf_target': 2,
'core_version_group': 18,
'core_version_group': 14,
'min_relay_fee': 0.00001,
'chain_lookups': 'local',
},
'navcoin': {
@@ -1664,7 +1726,7 @@ def main():
addTorSettings(settings, tor_control_password)
for coin in settings['chainclients']:
modify_tor_config(settings, coin, tor_control_password, enable=True)
modify_tor_config(settings, coin, tor_control_password, enable=True, extra_opts=extra_opts)
with open(config_path, 'w') as fp:
json.dump(settings, fp, indent=4)
@@ -1677,7 +1739,7 @@ def main():
settings = load_config(config_path)
settings['use_tor'] = False
for coin in settings['chainclients']:
modify_tor_config(settings, coin, tor_control_password=None, enable=False)
modify_tor_config(settings, coin, tor_control_password=None, enable=False, extra_opts=extra_opts)
with open(config_path, 'w') as fp:
json.dump(settings, fp, indent=4)
@@ -1774,9 +1836,11 @@ def main():
'max_delay_event': 50, # Max delay in seconds before reacting to an event
'check_progress_seconds': 60,
'check_watched_seconds': 60,
'check_expired_seconds': 60
'check_expired_seconds': 60,
'wallet_update_timeout': 10, # Seconds to wait for wallet page update
}
wshost: str = extra_opts.get('wshost', htmlhost)
if wshost != 'none':
settings['wshost'] = wshost
settings['wsport'] = UI_WS_PORT + port_offset

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright (c) 2019-2023 tecnovert
# Copyright (c) 2019-2024 tecnovert
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -21,12 +21,25 @@ from basicswap.basicswap import BasicSwap
from basicswap.http_server import HttpThread
from basicswap.contrib.websocket_server import WebsocketServer
logger = logging.getLogger()
logger.level = logging.DEBUG
if not len(logger.handlers):
logger.addHandler(logging.StreamHandler(sys.stdout))
swap_client = None
# TODO: deduplicate
known_coins = [
'particl',
'litecoin',
'bitcoin',
'namecoin',
'monero',
'pivx',
'dash',
'firo',
'navcoin',
]
def signal_handler(sig, frame):
@@ -112,7 +125,7 @@ def ws_message_received(client, server, message):
swap_client.log.debug(f'ws_message_received {client["id"]} {message}')
def runClient(fp, data_dir, chain):
def runClient(fp, data_dir, chain, start_only_coins):
global swap_client
daemons = []
pids = []
@@ -143,6 +156,9 @@ def runClient(fp, data_dir, chain):
try:
# Try start daemons
for c, v in settings['chainclients'].items():
if len(start_only_coins) > 0 and c not in start_only_coins:
continue
try:
coin_id = swap_client.getCoinIdFromName(c)
display_name = getCoinName(coin_id)
@@ -160,13 +176,24 @@ def runClient(fp, data_dir, chain):
if v['manage_wallet_daemon'] is True:
swap_client.log.info(f'Starting {display_name} wallet daemon')
daemon_addr = '{}:{}'.format(v['rpchost'], v['rpcport'])
swap_client.log.info('daemon-address: {}'.format(daemon_addr))
trusted_daemon: bool = swap_client.getXMRTrustedDaemon(coin_id, v['rpchost'])
opts = ['--daemon-address', daemon_addr, ]
proxy_log_str = ''
proxy_host, proxy_port = swap_client.getXMRWalletProxy(coin_id, v['rpchost'])
if proxy_host:
proxy_log_str = ' through proxy'
opts += ['--proxy', f'{proxy_host}:{proxy_port}', ]
swap_client.log.info('daemon-address: {} ({}){}'.format(daemon_addr, 'trusted' if trusted_daemon else 'untrusted', proxy_log_str))
daemon_rpcuser = v.get('rpcuser', '')
daemon_rpcpass = v.get('rpcpassword', '')
if daemon_rpcuser != '':
opts.append('--daemon-login')
opts.append(daemon_rpcuser + ':' + daemon_rpcpass)
opts.append('--trusted-daemon' if trusted_daemon else '--untrusted-daemon')
filename = 'monero-wallet-rpc' + ('.exe' if os.name == 'nt' else '')
daemons.append(startXmrWalletDaemon(v['datadir'], v['bindir'], filename, opts))
pid = daemons[-1].pid
@@ -189,28 +216,33 @@ def runClient(fp, data_dir, chain):
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
swap_client.start()
if 'htmlhost' in settings:
swap_client.log.info('Starting http server at http://%s:%d.' % (settings['htmlhost'], settings['htmlport']))
allow_cors = settings['allowcors'] if 'allowcors' in settings else cfg.DEFAULT_ALLOW_CORS
thread_http = HttpThread(fp, settings['htmlhost'], settings['htmlport'], allow_cors, swap_client)
threads.append(thread_http)
thread_http.start()
if len(start_only_coins) > 0:
logger.info(f'Only running {start_only_coins}. Manually exit with Ctrl + c when ready.')
while not swap_client.delay_event.wait(0.5):
pass
else:
swap_client.start()
if 'htmlhost' in settings:
swap_client.log.info('Starting http server at http://%s:%d.' % (settings['htmlhost'], settings['htmlport']))
allow_cors = settings['allowcors'] if 'allowcors' in settings else cfg.DEFAULT_ALLOW_CORS
thread_http = HttpThread(fp, settings['htmlhost'], settings['htmlport'], allow_cors, swap_client)
threads.append(thread_http)
thread_http.start()
if 'wshost' in settings:
ws_url = 'ws://{}:{}'.format(settings['wshost'], settings['wsport'])
swap_client.log.info(f'Starting ws server at {ws_url}.')
if 'wshost' in settings:
ws_url = 'ws://{}:{}'.format(settings['wshost'], settings['wsport'])
swap_client.log.info(f'Starting ws server at {ws_url}.')
swap_client.ws_server = WebsocketServer(host=settings['wshost'], port=settings['wsport'])
swap_client.ws_server.set_fn_new_client(ws_new_client)
swap_client.ws_server.set_fn_client_left(ws_client_left)
swap_client.ws_server.set_fn_message_received(ws_message_received)
swap_client.ws_server.run_forever(threaded=True)
swap_client.ws_server = WebsocketServer(host=settings['wshost'], port=settings['wsport'])
swap_client.ws_server.set_fn_new_client(ws_new_client)
swap_client.ws_server.set_fn_client_left(ws_client_left)
swap_client.ws_server.set_fn_message_received(ws_message_received)
swap_client.ws_server.run_forever(threaded=True)
logger.info('Exit with Ctrl + c.')
while not swap_client.delay_event.wait(0.5):
swap_client.update()
logger.info('Exit with Ctrl + c.')
while not swap_client.delay_event.wait(0.5):
swap_client.update()
except Exception as ex:
traceback.print_exc()
@@ -267,18 +299,20 @@ def printVersion():
def printHelp():
logger.info('Usage: basicswap-run ')
logger.info('\n--help, -h Print help.')
logger.info('--version, -v Print version.')
logger.info('--datadir=PATH Path to basicswap data directory, default:{}.'.format(cfg.BASICSWAP_DATADIR))
logger.info('--mainnet Run in mainnet mode.')
logger.info('--testnet Run in testnet mode.')
logger.info('--regtest Run in regtest mode.')
print('Usage: basicswap-run ')
print('\n--help, -h Print help.')
print('--version, -v Print version.')
print('--datadir=PATH Path to basicswap data directory, default:{}.'.format(cfg.BASICSWAP_DATADIR))
print('--mainnet Run in mainnet mode.')
print('--testnet Run in testnet mode.')
print('--regtest Run in regtest mode.')
print('--startonlycoin Only start the provides coin daemon/s, use this if a chain requires extra processing.')
def main():
data_dir = None
chain = 'mainnet'
start_only_coins = set()
for v in sys.argv[1:]:
if len(v) < 2 or v[0] != '-':
@@ -299,17 +333,20 @@ def main():
printHelp()
return 0
if name == 'testnet':
chain = 'testnet'
continue
if name == 'regtest':
chain = 'regtest'
if name in ('mainnet', 'testnet', 'regtest'):
chain = name
continue
if len(s) == 2:
if name == 'datadir':
data_dir = os.path.expanduser(s[1])
continue
if name == 'startonlycoin':
for coin in [s.lower() for s in s[1].split(',')]:
if coin not in known_coins:
raise ValueError(f'Unknown coin: {coin}')
start_only_coins.add(coin)
continue
logger.warning('Unknown argument %s', v)
@@ -323,7 +360,7 @@ def main():
with open(os.path.join(data_dir, 'basicswap.log'), 'a') as fp:
logger.info(os.path.basename(sys.argv[0]) + ', version: ' + __version__ + '\n\n')
runClient(fp, data_dir, chain)
runClient(fp, data_dir, chain, start_only_coins)
logger.info('Done.')
return swap_client.fail_code if swap_client is not None else 0

12
doc/ci.md Normal file
View File

@@ -0,0 +1,12 @@
## Run Cirrus CI Tests Locally
Install cirrus-cli:
https://github.com/cirruslabs/cirrus-cli/blob/master/INSTALL.md
Run:
cd basicswap
cirrus run -v -o simple

View File

@@ -140,7 +140,7 @@ Continue from the [Run Using Docker](#run-using-docker) section.
### Ubuntu Setup:
apt-get install -y wget python3-pip gnupg unzip protobuf-compiler automake libtool pkg-config curl jq
apt-get install -y wget git python3-venv python3-pip gnupg unzip protobuf-compiler automake libtool pkg-config curl jq
### OSX Setup:
@@ -162,7 +162,7 @@ Close the terminal and open a new one to update the python symlinks.
python3 -m venv "$SWAP_DATADIR/venv"
. $SWAP_DATADIR/venv/bin/activate && python -V
cd $SWAP_DATADIR
wget -O coincurve-anonswap.zip https://github.com/tecnovert/coincurve/archive/refs/tags/anonswap_v0.1.zip
wget -O coincurve-anonswap.zip https://github.com/tecnovert/coincurve/archive/refs/tags/anonswap_v0.2.zip
unzip -d coincurve-anonswap coincurve-anonswap.zip
mv ./coincurve-anonswap/*/{.,}* ./coincurve-anonswap || true
cd $SWAP_DATADIR/coincurve-anonswap
@@ -173,6 +173,7 @@ Close the terminal and open a new one to update the python symlinks.
git clone https://github.com/tecnovert/basicswap.git
cd $SWAP_DATADIR/basicswap
If installed on OSX, you may need to install additional root ssl certificates for the ssl module.
From https://pypi.org/project/certifi/
@@ -189,10 +190,11 @@ Prepare the datadir:
CURRENT_XMR_HEIGHT=$(curl https://localmonero.co/blocks/api/get_stats | jq .height)
basicswap-prepare --datadir=$SWAP_DATADIR --withcoins=monero --xmrrestoreheight=$CURRENT_XMR_HEIGHT
OR using a remote/public XMR daemon (not recommended):
XMR_RPC_HOST="node.xmr.to" BASE_XMR_RPC_PORT=18081 basicswap-prepare --datadir=$SWAP_DATADIR --withcoins=monero --xmrrestoreheight=$CURRENT_XMR_HEIGHT
OR using a local XMR daemon:
basicswap-prepare --datadir=$SWAP_DATADIR --withcoins=monero --xmrrestoreheight=$CURRENT_XMR_HEIGHT
Record the mnemonic from the output of the above command.
@@ -200,6 +202,7 @@ Start Basicswap:
basicswap-run --datadir=$SWAP_DATADIR
Open in browser: `http://localhost:12700`
It may take a few minutes to start as the coin daemons are started before the http interface.

View File

@@ -111,9 +111,7 @@ Test:
## Run One Test
```
pytest -v -s tests/basicswap/test_xmr.py::Test::test_02_leader_recover_a_lock_tx
```
pytest -v -s tests/basicswap/test_xmr.py::Test::test_02_leader_recover_a_lock_tx
## Private Offers
@@ -127,6 +125,17 @@ To send a private offer:
Nodes will ignore offers sent on keys other than the network key or keys created for offer-receiving.
## Coin reindexing
export COINDATA_PATH=/var/data/coinswaps
cd $COINDATA_PATH/bin/firo
./firod -reindex -datadir=$COINDATA_PATH/firo -nodebuglogfile -printtoconsole > /tmp/firo.log
Observe progress with
tail -f /tmp/firo.log
## FAQ

View File

@@ -31,7 +31,7 @@ xu {
C note C2
[label="The XmrBidLockSpendTxMessage contains the script-coin-lock-tx and proof the bidder can sign it.",
textbgcolor="#FFFFCC"];
B =>> N [label="Sends script-coin-lock-tx"],
B =>> N [label="Sends script-coin-lock-tx"];
O abox O [label="Bid Script coin spend tx valid"];
O abox O [label="Exchanged script lock spend tx msg"];
O => O [label="Wait for script-coin-lock-tx to confirm"];

View File

@@ -1,4 +1,73 @@
0.0.68
0.12.7
==============
- basicswap-prepare
- Sets --usetorproxy automatically when tor is enabled on existing installs for commands that access the network.
- Disable with --notorproxy
- Switch the LTC download URL to github (works over Tor)
- Sets `wshost` to match `htmlhost` by default
- doc: Simplify docker tor install notes.
- Basicswap will set monero-wallet-rpc proxy when Tor is enabled if the host ip setting (`rpchost`) for the monerod instance is not a private ip.
- Works for automatically selected daemons too.
- Override with a `use_tor` parameter in the Monero section of basicswap.json.
- Basicswap sets monero-wallet-rpc `--trusted-daemon` if the host ip setting for the monerod instance is a private ip.
- Override with the `trusted_daemon` parameter in the Monero section of basicswap.json.
- Defaults to auto.
- Override in basicswap-prepare with `--trustremotenode`
- Add settings in basicswap.json to set Monero rpc timeouts
- `rpctimeout`, `walletrpctimeout` and `walletrpctimeoutlong` in the Monero section of basicswap.json.
- `wallet_update_timeout` in basicswap.json to set how long the wallet ui page waits for an rpc response.
- ui: Renamed unconfirmed balance to pending and include immature balance in pending.
0.12.6
==============
- ui: Display count of locked UTXOs on wallet page.
- Only shows if locked UTXOs is > 0
0.12.5
==============
- Unlock wallets logs an error when failing to unlock a wallet.
- Fixed bug where failed unlock prevents processing incoming smsg messages.
0.12.4
==============
- LTC creates a new wallet to hold MWEB balance.
- MWEB wallet should be be automatically created at startup or when unlocked if system is encrypted.
0.12.3
==============
- New basicswap-run parameter startonlycoin.
- If set basicswap-run will only start the specified coin daemon/s.
0.12.2
==============
- Updated coincurve and libsecp256k1 versions.
- Avoids needing secp256k1_generator_const_g as it's not accessible from a dll.
- Fix missing ripemd160 on some systems.
0.12.1
==============
- Firo and Navcoin send utxo outpoints with proof of funds address+sig.
- Avoids missing scantxoutset command
- Firo Bids will be incompatible with older versions.
- Bids for other coins should still work between versions.
- Firo uses workarounds for missing getblock verbosity and rescanblockchain
- Coins without segwit can be used in reverse adaptor sig swaps.
0.11.68
==============
- Temporarily disabled Navcoin.
@@ -6,7 +75,7 @@
- Fixed bug where requesting a new XMR subaddress would return an old one.
0.0.67
0.11.67
==============
- Added support for p2sh-p2wsh coins
@@ -16,13 +85,13 @@
- Not backwards compatible with previous versions.
0.0.66
0.11.66
==============
- Fixed bugs in getLinkedMessageId and validateSwapType.
0.0.65
0.11.65
==============
- smsg: Outbox messages are removed when expired.
@@ -33,7 +102,7 @@
- ui: Show ITX and PTX status for adaptor sig type swaps.
0.0.64
0.11.64
==============
- protocol: Added reversed Adaptor sig protocol.
@@ -44,7 +113,7 @@
- Not backwards compatible with previous versions.
0.0.63
0.11.63
==============
- cores: Raised Particl and Monero daemon version.
@@ -53,7 +122,7 @@
- Abandoning a bid stops all processing.
0.0.62
0.11.62
==============
- ui: Persistent filters
@@ -62,13 +131,13 @@
- Adaptor signature swaps are not backwards compatible with previous versions.
0.0.61
0.11.61
==============
- GUI 2.0
0.0.60
0.11.60
==============
- Accepted bids will timeout if the peer does not respond within an hour after the bid expires.
@@ -80,7 +149,7 @@
BITCOIN_FASTSYNC_FILE env var in the datadir without checking it's size or signature.
0.0.59
0.11.59
==============
- Added total_bids_value_multiplier option to automation strategies.
@@ -104,7 +173,7 @@
- ui: Can edit offer automation strategy.
0.0.54
0.11.54
==============
- If the XMR daemon is busy the wallet can fail a transfer, later sending the tx unknown to bsx.

View File

@@ -16,40 +16,49 @@ basicswap-prepare can be configured to download all binaries through tor and to
Docker will create directories instead of files if these don't exist.
mkdir -p $COINDATA_PATH/tor
touch $COINDATA_PATH/tor/torrc
echo 'SocksPort 0.0.0.0:9050' > $COINDATA_PATH/tor/torrc
#### For a new install
Use the `--usetorproxy` argument to download the coin binaries over tor, then enable tor with `--enabletor`.
Note that some download links, notably for Litecoin, are unreachable when using tor.
If running through docker start the tor container with the following command as the torrc configuration file won't exist yet.
docker compose -f docker-compose_with_tor.yml run --rm swapclient \
basicswap-prepare --usetorproxy --datadir=/coindata --withcoins=monero,particl
docker compose -f docker-compose_with_tor.yml run --name tor --rm tor \
tor --allow-missing-torrc --SocksPort 0.0.0.0:9050
docker compose -f docker-compose_with_tor.yml run --rm swapclient \
basicswap-prepare --enabletor --datadir=/coindata
docker compose -f docker-compose_with_tor.yml run -e TOR_PROXY_HOST=172.16.238.200 --rm swapclient \
basicswap-prepare --usetorproxy --datadir=/coindata --withcoins=monero,particl
The `--enabletor` option will add config to the torrc file, the tor container must afterwards be stopped to load the new config:
docker compose -f docker-compose_with_tor.yml stop
Start Basicswap with:
docker compose -f docker-compose_with_tor.yml up
#### Enable tor on an existing datadir
docker compose -f docker-compose_with_tor.yml run -e TOR_PROXY_HOST=172.16.238.200 --rm swapclient \
basicswap-prepare --datadir=/coindata --enabletor
docker compose -f docker-compose_with_tor.yml run --rm swapclient \
basicswap-prepare --datadir=/coindata --enabletor
docker compose -f docker-compose_with_tor.yml stop
#### Disable tor on an existing datadir
docker compose -f docker-compose_with_tor.yml run --rm swapclient \
basicswap-prepare --datadir=/coindata --disabletor
basicswap-prepare --datadir=/coindata --disabletor
docker compose -f docker-compose_with_tor.yml stop
#### Update coin release
docker compose -f docker-compose_with_tor.yml up -d tor
docker compose -f docker-compose_with_tor.yml run -e TOR_PROXY_HOST=172.16.238.200 --rm swapclient \
basicswap-prepare --usetorproxy --datadir=/coindata --preparebinonly --withcoins=bitcoin
docker compose -f docker-compose_with_tor.yml run --rm swapclient \
basicswap-prepare --usetorproxy --datadir=/coindata --preparebinonly --withcoins=bitcoin
docker compose -f docker-compose_with_tor.yml stop

View File

@@ -1,4 +1,5 @@
HTML_PORT=127.0.0.1:12700:12700
WS_PORT=127.0.0.1:11700:11700
#COINDATA_PATH=/var/data/coinswaps
TOR_PROXY_HOST=172.16.238.200
TZ=UTC

View File

@@ -5,6 +5,8 @@ services:
image: i_swapclient
container_name: swapclient
stop_grace_period: 5m
depends_on:
- tor
build:
context: ../
volumes:

View File

@@ -15,7 +15,7 @@ RUN wget -O protobuf_src.tar.gz https://github.com/protocolbuffers/protobuf/rele
make -j$(nproc) install && \
ldconfig
ARG COINCURVE_VERSION=v0.1
ARG COINCURVE_VERSION=v0.2
RUN wget -O coincurve-anonswap.zip https://github.com/tecnovert/coincurve/archive/refs/tags/anonswap_$COINCURVE_VERSION.zip && \
unzip coincurve-anonswap.zip && \
mv ./coincurve-anonswap_$COINCURVE_VERSION ./coincurve-anonswap && \

View File

@@ -26,7 +26,7 @@
(define libsecp256k1-anonswap
(package
(name "libsecp256k1-anonswap")
(version "anonswap_v0.1")
(version "anonswap_v0.2")
(source (origin
(method git-fetch)
(uri (git-reference
@@ -34,15 +34,17 @@
(commit version)))
(sha256
(base32
"1lrcc5gjywlzvrgwzifva4baa2nsvwr3h0wmkc71q0zhag9pjbah"))
"1r07rkrw5qsnc5v1q7cb0zfs1cr62fqwq7kd2v8650g6ha4k5r8i"))
(file-name (git-file-name name version))))
(build-system gnu-build-system)
(arguments
'(#:configure-flags '("--enable-shared"
"--disable-dependency-tracking"
"--with-valgrind=no"
"--enable-experimental"
"--with-pic"
"--enable-module-extrakeys"
"--enable-module-recovery"
"--enable-module-schnorrsig"
"--enable-experimental"
"--enable-module-ecdh"
"--enable-benchmark=no"
"--enable-tests=no"
@@ -50,6 +52,7 @@
"--enable-module-generator"
"--enable-module-dleag"
"--enable-module-ecdsaotves"
"--with-valgrind=no"
)))
(native-inputs
(list autoconf automake libtool))
@@ -63,7 +66,7 @@
(define python-coincurve-anonswap
(package
(name "python-coincurve-anonswap")
(version "anonswap_v0.1")
(version "anonswap_v0.2")
(source
(origin
(method git-fetch)
@@ -74,7 +77,7 @@
(file-name
(git-file-name name version))
(sha256
(base32 "0vyzvpp2s21js01185qbm1lgs4ps4hki2d6yzprfsjqap1i5qhmp"))))
(base32 "08fz02afh88m83axfm8jsgq1c65mw1f3g07x9hz361vblvqjwzqh"))))
(build-system python-build-system)
(arguments
'(#:tests? #f ;XXX fails to load "libsecp256k1.dll"
@@ -117,15 +120,15 @@
(define-public basicswap
(package
(name "basicswap")
(version "0.11.66")
(version "0.12.1")
(source (origin
(method git-fetch)
(uri (git-reference
(url "https://github.com/tecnovert/basicswap")
(commit "7735c9733a818a2cc13e1b5eaa78f81caa07d7d6")))
(commit "15bf9b2187d3b8a03939e61b4c3ebf4d90fcc919")))
(sha256
(base32
"1hps0hr25b8hgwz8q7jfa4d67sp8xvpn6028iss9mrqp72385aan"))
"14gn6156x53c6panxdnd1awkd23jxnihvbqy886j66w5js3b5i8h"))
(file-name (git-file-name name version))))
(build-system python-build-system)

View File

@@ -293,7 +293,7 @@ def main():
received_offers = read_json_api(args.port, 'offers', {'active': 'active', 'include_sent': False, 'coin_from': coin_from_data['id'], 'coin_to': coin_to_data['id']})
print('received_offers', received_offers)
TODO - adjust rates based on extisting offers
TODO - adjust rates based on existing offers
"""
rates = read_json_api('rates', {'coin_from': coin_from_data['id'], 'coin_to': coin_to_data['id']})
@@ -326,6 +326,8 @@ def main():
if args.debug:
print('offer data {}'.format(offer_data))
new_offer = read_json_api('offers/new', offer_data)
if 'error' in new_offer:
raise ValueError('Server failed to create offer: {}'.format(new_offer['error']))
print('New offer: {}'.format(new_offer['offer_id']))
if 'offers' not in script_state:
script_state['offers'] = {}
@@ -516,6 +518,8 @@ def main():
if args.debug:
print('Creating bid: {}'.format(bid_data))
new_bid = read_json_api('bids/new', bid_data)
if 'error' in new_bid:
raise ValueError('Server failed to create bid: {}'.format(new_bid['error']))
print('New bid: {} on offer {}'.format(new_bid['bid_id'], offer['offer_id']))
bid_id = new_bid['bid_id']

View File

@@ -73,7 +73,9 @@ def prepareDataDir(datadir, node_id, conf_file, dir_prefix, base_p2p_port=BASE_P
fp.write('wallet=wallet.dat\n')
fp.write('findpeers=0\n')
if base_p2p_port == BASE_PORT: # Particl
if base_p2p_port == BTC_BASE_PORT:
fp.write('deprecatedrpc=create_bdb\n')
elif base_p2p_port == BASE_PORT: # Particl
fp.write('zmqpubsmsg=tcp://127.0.0.1:{}\n'.format(BASE_ZMQ_PORT + node_id))
# minstakeinterval=5 # Using walletsettings stakelimit instead
fp.write('stakethreadconddelayms=1000\n')
@@ -228,6 +230,14 @@ def wait_for_none_active(delay_event, port, wait_for=30):
raise ValueError('wait_for_none_active timed out.')
def abandon_all_swaps(delay_event, swap_client) -> None:
logging.info('abandon_all_swaps')
for bid in swap_client.listBids(sent=True):
swap_client.abandonBid(bid[2])
for bid in swap_client.listBids(sent=False):
swap_client.abandonBid(bid[2])
def waitForNumOffers(delay_event, port, offers, wait_for=20):
for i in range(wait_for):
if delay_event.is_set():

View File

@@ -47,6 +47,11 @@ BITCOIN_TOR_PORT_BASE = int(os.getenv('BITCOIN_TOR_PORT_BASE', BTC_BASE_TOR_PORT
LITECOIN_RPC_PORT_BASE = int(os.getenv('LITECOIN_RPC_PORT_BASE', LTC_BASE_RPC_PORT))
FIRO_BASE_PORT = 34832
FIRO_BASE_RPC_PORT = 35832
FIRO_RPC_PORT_BASE = int(os.getenv('FIRO_RPC_PORT_BASE', FIRO_BASE_RPC_PORT))
XMR_BASE_P2P_PORT = 17792
XMR_BASE_RPC_PORT = 29798
XMR_BASE_WALLET_RPC_PORT = 29998
@@ -88,6 +93,7 @@ def run_prepare(node_id, datadir_path, bins_path, with_coins, mnemonic_in=None,
os.environ['PART_RPC_PORT'] = str(PARTICL_RPC_PORT_BASE)
os.environ['BTC_RPC_PORT'] = str(BITCOIN_RPC_PORT_BASE)
os.environ['LTC_RPC_PORT'] = str(LITECOIN_RPC_PORT_BASE)
os.environ['FIRO_RPC_PORT'] = str(FIRO_RPC_PORT_BASE)
os.environ['XMR_RPC_USER'] = 'xmr_user'
os.environ['XMR_RPC_PWD'] = 'xmr_pwd'
@@ -234,6 +240,33 @@ def run_prepare(node_id, datadir_path, bins_path, with_coins, mnemonic_in=None,
for opt in EXTRA_CONFIG_JSON.get('pivx{}'.format(node_id), []):
fp.write(opt + '\n')
if 'firo' in coins_array:
# Pruned nodes don't provide blocks
with open(os.path.join(datadir_path, 'firo', 'firo.conf'), 'r') as fp:
lines = fp.readlines()
with open(os.path.join(datadir_path, 'firo', 'firo.conf'), 'w') as fp:
for line in lines:
if not line.startswith('prune'):
fp.write(line)
fp.write('port={}\n'.format(FIRO_BASE_PORT + node_id + port_ofs))
fp.write('bind=127.0.0.1\n')
fp.write('dnsseed=0\n')
fp.write('discover=0\n')
fp.write('listenonion=0\n')
fp.write('upnp=0\n')
if use_rpcauth:
salt = generate_salt(16)
rpc_user = 'test_firo_' + str(node_id)
rpc_pass = 'test_firo_pwd_' + str(node_id)
fp.write('rpcauth={}:{}${}\n'.format(rpc_user, salt, password_to_hmac(salt, rpc_pass)))
settings['chainclients']['firo']['rpcuser'] = rpc_user
settings['chainclients']['firo']['rpcpassword'] = rpc_pass
for ip in range(num_nodes):
if ip != node_id:
fp.write('connect=127.0.0.1:{}\n'.format(FIRO_BASE_PORT + ip + port_ofs))
for opt in EXTRA_CONFIG_JSON.get('firo{}'.format(node_id), []):
fp.write(opt + '\n')
if 'monero' in coins_array:
with open(os.path.join(datadir_path, 'monero', 'monerod.conf'), 'a') as fp:
fp.write('p2p-bind-ip=127.0.0.1\n')

View File

@@ -672,7 +672,7 @@ class Test(unittest.TestCase):
# Verify expected inputs were used
bid, offer = swap_clients[2].getBidAndOffer(bid_id)
assert (bid.initiate_tx)
wtx = ci_from.rpc_callback('gettransaction', [bid.initiate_tx.txid.hex(),])
wtx = ci_from.rpc_wallet('gettransaction', [bid.initiate_tx.txid.hex(),])
itx_after = ci_from.describeTx(wtx['hex'])
assert (len(itx_after['vin']) == len(itx_decoded['vin']))
for i, txin in enumerate(itx_decoded['vin']):

View File

@@ -13,7 +13,6 @@ import unittest
import basicswap.config as cfg
from basicswap.basicswap import (
Coins,
TxStates,
SwapTypes,
BidStates,
DebugTypes,
@@ -22,7 +21,6 @@ from basicswap.basicswap_util import (
TxLockTypes,
)
from basicswap.util import (
COIN,
make_int,
format_amount,
)
@@ -39,14 +37,11 @@ from tests.basicswap.common import (
make_rpc_func,
TEST_HTTP_PORT,
wait_for_offer,
wait_for_balance,
wait_for_unspent,
wait_for_in_progress,
wait_for_bid_tx_state,
)
from basicswap.contrib.test_framework.messages import (
from basicswap.interface.contrib.firo_test_framework.mininode import (
FromHex,
CTransaction,
set_regtest,
)
from bin.basicswap_run import startDaemon
from basicswap.contrib.rpcauth import generate_salt, password_to_hmac
@@ -97,11 +92,13 @@ def prepareDataDir(datadir, node_id, conf_file, dir_prefix, base_p2p_port, base_
fp.write('fallbackfee=0.01\n')
fp.write('acceptnonstdtxn=0\n')
'''
# qa/rpc-tests/segwit.py
fp.write('prematurewitness=1\n')
fp.write('walletprematurewitness=1\n')
fp.write('blockversion=4\n')
fp.write('promiscuousmempoolflags=517\n')
'''
for i in range(0, num_nodes):
if node_id == i:
@@ -150,6 +147,10 @@ class Test(BaseTest):
@classmethod
def prepareExtraCoins(cls):
# Raise MTP_SWITCH_TIME and PP_SWITCH_TIME in mininode.py
set_regtest()
if cls.restore_instance:
void_block_rewards_pubkey = cls.getRandomPubkey()
cls.firo_addr = cls.swap_clients[0].ci(Coins.FIRO).pubkey_to_address(void_block_rewards_pubkey)
@@ -168,7 +169,8 @@ class Test(BaseTest):
# Set future block rewards to nowhere (a random address), so wallet amounts stay constant
void_block_rewards_pubkey = cls.getRandomPubkey()
cls.firo_addr = cls.swap_clients[0].ci(Coins.FIRO).pubkey_to_address(void_block_rewards_pubkey)
num_blocks = 100
chain_height = callnoderpc(0, 'getblockcount', base_rpc_port=FIRO_BASE_RPC_PORT)
num_blocks = 1352 - chain_height # Activate CTLV (bip65)
logging.info('Mining %d Firo blocks to %s', num_blocks, cls.firo_addr)
callnoderpc(0, 'generatetoaddress', [num_blocks, cls.firo_addr], base_rpc_port=FIRO_BASE_RPC_PORT)
@@ -189,7 +191,7 @@ class Test(BaseTest):
'rpcpassword': 'test_pass' + str(node_id),
'datadir': os.path.join(datadir, 'firo_' + str(node_id)),
'bindir': FIRO_BINDIR,
'use_csv': True,
'use_csv': False,
'use_segwit': False,
}
@@ -207,6 +209,9 @@ class Test(BaseTest):
def callnoderpc(self, method, params=[], wallet=None, node_id=0):
return callnoderpc(node_id, method, params, wallet, base_rpc_port=FIRO_BASE_RPC_PORT)
def mineBlock(self, num_blocks: int = 1):
self.callnoderpc('generatetoaddress', [num_blocks, self.firo_addr])
def test_001_firo(self):
logging.info('---------- Test {} segwit'.format(self.test_coin_from.name))
@@ -216,6 +221,7 @@ class Test(BaseTest):
Txns spending segwit utxos don't get mined.
'''
return
swap_clients = self.swap_clients
@@ -251,7 +257,8 @@ class Test(BaseTest):
decoded_tx = CTransaction()
decoded_tx = FromHex(decoded_tx, tx_funded)
decoded_tx.vin[0].scriptSig = bytes.fromhex('16' + addr_witness_info['hex'])
txid_with_scriptsig = decoded_tx.rehash()
decoded_tx.rehash()
txid_with_scriptsig = decoded_tx.hash
tx_funded_decoded = firoCli(f'decoderawtransaction {tx_funded}')
tx_signed_decoded = firoCli(f'decoderawtransaction {tx_signed}')
@@ -268,148 +275,45 @@ class Test(BaseTest):
assert ('490ba1e2c3894d5534c467141ee3cdf77292c362' == ci.getWalletSeedID())
assert swap_client.checkWalletSeed(self.test_coin_from) is True
def test_02_part_coin(self):
logging.info('---------- Test PART to {}'.format(self.test_coin_from.name))
if not self.test_atomic:
logging.warning('Skipping test')
return
swap_clients = self.swap_clients
def test_008_gettxout(self):
logging.info('---------- Test {} gettxout'.format(self.test_coin_from.name))
offer_id = swap_clients[0].postOffer(Coins.PART, self.test_coin_from, 100 * COIN, 0.1 * COIN, 100 * COIN, SwapTypes.SELLER_FIRST)
swap_client = self.swap_clients[0]
wait_for_offer(test_delay_event, swap_clients[1], offer_id)
offer = swap_clients[1].getOffer(offer_id)
bid_id = swap_clients[1].postBid(offer_id, offer.amount_from)
# First address sometimes has a balance already
addr_plain = self.callnoderpc('getnewaddress', ['gettxout test',])
wait_for_bid(test_delay_event, swap_clients[0], bid_id)
swap_clients[0].acceptBid(bid_id)
addr_plain1 = self.callnoderpc('getnewaddress', ['gettxout test 1',])
wait_for_in_progress(test_delay_event, swap_clients[1], bid_id, sent=True)
wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, wait_for=60)
wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, sent=True, wait_for=60)
txid = self.callnoderpc('sendtoaddress', [addr_plain1, 1.0])
assert len(txid) == 64
js_0 = read_json_api(1800)
js_1 = read_json_api(1801)
assert (js_0['num_swapping'] == 0 and js_0['num_watched_outputs'] == 0)
assert (js_1['num_swapping'] == 0 and js_1['num_watched_outputs'] == 0)
self.mineBlock()
def test_03_coin_part(self):
logging.info('---------- Test {} to PART'.format(self.test_coin_from.name))
swap_clients = self.swap_clients
unspents = self.callnoderpc('listunspent', [0, 999999999, [addr_plain1,]])
assert (len(unspents) == 1)
offer_id = swap_clients[1].postOffer(self.test_coin_from, Coins.PART, 10 * COIN, 9.0 * COIN, 10 * COIN, SwapTypes.SELLER_FIRST)
utxo = unspents[0]
txout = self.callnoderpc('gettxout', [utxo['txid'], utxo['vout']])
assert (addr_plain1 in txout['scriptPubKey']['addresses'])
# Spend
addr_plain2 = self.callnoderpc('getnewaddress', ['gettxout test 2',])
tx_funded = self.callnoderpc('createrawtransaction', [[{'txid': utxo['txid'], 'vout': utxo['vout']}], {addr_plain2: 0.99}])
tx_signed = self.callnoderpc('signrawtransaction', [tx_funded,])['hex']
self.callnoderpc('sendrawtransaction', [tx_signed,])
wait_for_offer(test_delay_event, swap_clients[0], offer_id)
offer = swap_clients[0].getOffer(offer_id)
bid_id = swap_clients[0].postBid(offer_id, offer.amount_from)
# utxo should be unavailable when spent in the mempool
txout = self.callnoderpc('gettxout', [utxo['txid'], utxo['vout']])
assert (txout is None)
wait_for_bid(test_delay_event, swap_clients[1], bid_id)
swap_clients[1].acceptBid(bid_id)
self.mineBlock()
wait_for_in_progress(test_delay_event, swap_clients[0], bid_id, sent=True)
ci = swap_client.ci(Coins.FIRO)
require_amount: int = ci.make_int(1)
funds_proof = ci.getProofOfFunds(require_amount, 'test'.encode('utf-8'))
wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, sent=True, wait_for=60)
wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, wait_for=60)
js_0 = read_json_api(1800)
js_1 = read_json_api(1801)
assert (js_0['num_swapping'] == 0 and js_0['num_watched_outputs'] == 0)
assert (js_1['num_swapping'] == 0 and js_1['num_watched_outputs'] == 0)
def test_04_coin_btc(self):
logging.info('---------- Test {} to BTC'.format(self.test_coin_from.name))
swap_clients = self.swap_clients
offer_id = swap_clients[0].postOffer(self.test_coin_from, Coins.BTC, 10 * COIN, 0.1 * COIN, 10 * COIN, SwapTypes.SELLER_FIRST)
wait_for_offer(test_delay_event, swap_clients[1], offer_id)
offer = swap_clients[1].getOffer(offer_id)
bid_id = swap_clients[1].postBid(offer_id, offer.amount_from)
wait_for_bid(test_delay_event, swap_clients[0], bid_id)
swap_clients[0].acceptBid(bid_id)
wait_for_in_progress(test_delay_event, swap_clients[1], bid_id, sent=True)
wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, wait_for=60)
wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, sent=True, wait_for=60)
js_0bid = read_json_api(1800, 'bids/{}'.format(bid_id.hex()))
js_0 = read_json_api(1800)
js_1 = read_json_api(1801)
assert (js_0['num_swapping'] == 0 and js_0['num_watched_outputs'] == 0)
assert (js_1['num_swapping'] == 0 and js_1['num_watched_outputs'] == 0)
def test_05_refund(self):
# Seller submits initiate txn, buyer doesn't respond
logging.info('---------- Test refund, {} to BTC'.format(self.test_coin_from.name))
swap_clients = self.swap_clients
offer_id = swap_clients[0].postOffer(self.test_coin_from, Coins.BTC, 10 * COIN, 0.1 * COIN, 10 * COIN, SwapTypes.SELLER_FIRST,
TxLockTypes.SEQUENCE_LOCK_BLOCKS, 10)
wait_for_offer(test_delay_event, swap_clients[1], offer_id)
offer = swap_clients[1].getOffer(offer_id)
bid_id = swap_clients[1].postBid(offer_id, offer.amount_from)
wait_for_bid(test_delay_event, swap_clients[0], bid_id)
swap_clients[1].abandonBid(bid_id)
swap_clients[0].acceptBid(bid_id)
wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, wait_for=60)
wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.BID_ABANDONED, sent=True, wait_for=60)
js_0 = read_json_api(1800)
js_1 = read_json_api(1801)
assert (js_0['num_swapping'] == 0 and js_0['num_watched_outputs'] == 0)
assert (js_1['num_swapping'] == 0 and js_1['num_watched_outputs'] == 0)
def test_06_self_bid(self):
logging.info('---------- Test same client, BTC to {}'.format(self.test_coin_from.name))
swap_clients = self.swap_clients
js_0_before = read_json_api(1800)
offer_id = swap_clients[0].postOffer(self.test_coin_from, Coins.BTC, 10 * COIN, 10 * COIN, 10 * COIN, SwapTypes.SELLER_FIRST)
wait_for_offer(test_delay_event, swap_clients[0], offer_id)
offer = swap_clients[0].getOffer(offer_id)
bid_id = swap_clients[0].postBid(offer_id, offer.amount_from)
wait_for_bid(test_delay_event, swap_clients[0], bid_id)
swap_clients[0].acceptBid(bid_id)
wait_for_bid_tx_state(test_delay_event, swap_clients[0], bid_id, TxStates.TX_REDEEMED, TxStates.TX_REDEEMED, wait_for=60)
wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, wait_for=60)
js_0 = read_json_api(1800)
assert (js_0['num_swapping'] == 0 and js_0['num_watched_outputs'] == 0)
assert (js_0['num_recv_bids'] == js_0_before['num_recv_bids'] + 1 and js_0['num_sent_bids'] == js_0_before['num_sent_bids'] + 1)
def test_07_error(self):
logging.info('---------- Test error, BTC to {}, set fee above bid value'.format(self.test_coin_from.name))
swap_clients = self.swap_clients
js_0_before = read_json_api(1800)
offer_id = swap_clients[0].postOffer(self.test_coin_from, Coins.BTC, 0.001 * COIN, 1.0 * COIN, 0.001 * COIN, SwapTypes.SELLER_FIRST)
wait_for_offer(test_delay_event, swap_clients[0], offer_id)
offer = swap_clients[0].getOffer(offer_id)
bid_id = swap_clients[0].postBid(offer_id, offer.amount_from)
wait_for_bid(test_delay_event, swap_clients[0], bid_id)
swap_clients[0].acceptBid(bid_id)
try:
swap_clients[0].getChainClientSettings(Coins.BTC)['override_feerate'] = 10.0
swap_clients[0].getChainClientSettings(Coins.FIRO)['override_feerate'] = 10.0
wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.BID_ERROR, wait_for=60)
swap_clients[0].abandonBid(bid_id)
finally:
del swap_clients[0].getChainClientSettings(Coins.BTC)['override_feerate']
del swap_clients[0].getChainClientSettings(Coins.FIRO)['override_feerate']
amount_proved = ci.verifyProofOfFunds(funds_proof[0], funds_proof[1], funds_proof[2], 'test'.encode('utf-8'))
assert (amount_proved >= require_amount)
def test_08_wallet(self):
logging.info('---------- Test {} wallet'.format(self.test_coin_from.name))
@@ -434,85 +338,6 @@ class Test(BaseTest):
json_rv = read_json_api(TEST_HTTP_PORT + 0, 'wallets/{}/createutxo'.format(self.test_coin_from.name.lower()), post_json)
assert (len(json_rv['txid']) == 64)
def ensure_balance(self, coin_type, node_id, amount):
tla = coin_type.name
js_w = read_json_api(1800 + node_id, 'wallets')
if float(js_w[tla]['balance']) < amount:
post_json = {
'value': amount,
'address': js_w[tla]['deposit_address'],
'subfee': False,
}
json_rv = read_json_api(1800, 'wallets/{}/withdraw'.format(tla.lower()), post_json)
assert (len(json_rv['txid']) == 64)
wait_for_balance(test_delay_event, 'http://127.0.0.1:{}/json/wallets/{}'.format(1800 + node_id, tla.lower()), 'balance', amount)
def test_10_prefunded_itx(self):
logging.info('---------- Test prefunded itx offer')
swap_clients = self.swap_clients
coin_from = Coins.FIRO
coin_to = Coins.BTC
swap_type = SwapTypes.SELLER_FIRST
ci_from = swap_clients[2].ci(coin_from)
ci_to = swap_clients[1].ci(coin_to)
tla_from = coin_from.name
# Prepare balance
self.ensure_balance(coin_from, 2, 10.0)
self.ensure_balance(coin_to, 1, 100.0)
js_w2 = read_json_api(1802, 'wallets')
post_json = {
'value': 10.0,
'address': read_json_api(1802, 'wallets/{}/nextdepositaddr'.format(tla_from.lower())),
'subfee': True,
}
json_rv = read_json_api(1802, 'wallets/{}/withdraw'.format(tla_from.lower()), post_json)
wait_for_balance(test_delay_event, 'http://127.0.0.1:1802/json/wallets/{}'.format(tla_from.lower()), 'balance', 9.0)
assert (len(json_rv['txid']) == 64)
# Create prefunded ITX
pi = swap_clients[2].pi(SwapTypes.XMR_SWAP)
js_w2 = read_json_api(1802, 'wallets')
swap_value = 10.0
if float(js_w2[tla_from]['balance']) < swap_value:
swap_value = js_w2[tla_from]['balance']
swap_value = ci_from.make_int(swap_value)
assert (swap_value > ci_from.make_int(9))
itx = pi.getFundedInitiateTxTemplate(ci_from, swap_value, True)
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(test_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)
offer_id = swap_clients[2].postOffer(coin_from, coin_to, swap_value, rate_swap, swap_value, swap_type, extra_options=extra_options)
wait_for_offer(test_delay_event, swap_clients[1], offer_id)
offer = swap_clients[1].getOffer(offer_id)
bid_id = swap_clients[1].postBid(offer_id, offer.amount_from)
wait_for_bid(test_delay_event, swap_clients[2], bid_id, BidStates.BID_RECEIVED)
swap_clients[2].acceptBid(bid_id)
wait_for_bid(test_delay_event, swap_clients[2], bid_id, BidStates.SWAP_COMPLETED, wait_for=120)
wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, sent=True, wait_for=120)
# Verify expected inputs were used
bid, offer = swap_clients[2].getBidAndOffer(bid_id)
assert (bid.initiate_tx)
wtx = ci_from.rpc_callback('gettransaction', [bid.initiate_tx.txid.hex(),])
itx_after = ci_from.describeTx(wtx['hex'])
assert (len(itx_after['vin']) == len(itx_decoded['vin']))
for i, txin in enumerate(itx_decoded['vin']):
assert (txin['txid'] == itx_after['vin'][i]['txid'])
assert (txin['vout'] == itx_after['vin'][i]['vout'])
def test_11_xmrswap_to(self):
logging.info('---------- Test xmr swap protocol to')
@@ -564,6 +389,30 @@ class Test(BaseTest):
wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.XMR_SWAP_FAILED_REFUNDED, wait_for=180)
wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.XMR_SWAP_FAILED_REFUNDED, sent=True)
def test_13_adsswap_reverse(self):
logging.info('---------- Test ads swap protocol reverse')
swap_clients = self.swap_clients
coin_from = Coins.FIRO
coin_to = Coins.BTC
swap_type = SwapTypes.XMR_SWAP
ci_from = swap_clients[0].ci(coin_from)
ci_to = swap_clients[1].ci(coin_to)
swap_value = ci_from.make_int(random.uniform(0.2, 20.0), r=1)
rate_swap = ci_to.make_int(random.uniform(0.2, 10.0), r=1)
offer_id = swap_clients[0].postOffer(coin_from, coin_to, swap_value, rate_swap, swap_value, swap_type)
wait_for_offer(test_delay_event, swap_clients[1], offer_id)
offer = swap_clients[1].getOffer(offer_id)
bid_id = swap_clients[1].postBid(offer_id, offer.amount_from)
wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.BID_RECEIVED)
swap_clients[0].acceptBid(bid_id)
wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.SWAP_COMPLETED, wait_for=120)
wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.SWAP_COMPLETED, sent=True, wait_for=120)
def test_101_full_swap(self):
logging.info('---------- Test {} to XMR'.format(self.test_coin_from.name))
if not self.test_xmr:

View File

@@ -902,7 +902,7 @@ class Test(TestFunctions):
# Verify expected inputs were used
bid, offer = swap_clients[2].getBidAndOffer(bid_id)
assert (bid.initiate_tx)
wtx = ci_from.rpc_callback('gettransaction', [bid.initiate_tx.txid.hex(),])
wtx = ci_from.rpc('gettransaction', [bid.initiate_tx.txid.hex(),])
itx_after = ci_from.describeTx(wtx['hex'])
assert (len(itx_after['vin']) == len(itx_decoded['vin']))
for i, txin in enumerate(itx_decoded['vin']):

View File

@@ -677,7 +677,7 @@ class Test(unittest.TestCase):
# Verify expected inputs were used
bid, offer = swap_clients[2].getBidAndOffer(bid_id)
assert (bid.initiate_tx)
wtx = ci_from.rpc_callback('gettransaction', [bid.initiate_tx.txid.hex(),])
wtx = ci_from.rpc('gettransaction', [bid.initiate_tx.txid.hex(),])
itx_after = ci_from.describeTx(wtx['hex'])
assert (len(itx_after['vin']) == len(itx_decoded['vin']))
for i, txin in enumerate(itx_decoded['vin']):

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright (c) 2019-2022 tecnovert
# Copyright (c) 2019-2024 tecnovert
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -91,7 +91,6 @@ class Test(unittest.TestCase):
prepareSystem.main()
self.assertEqual(cm.exception.code, 1)
logger.info('fake_stderr.getvalue() %s', fake_stderr.getvalue())
self.assertTrue('exists, exiting' in fake_stderr.getvalue())
logger.info('Test addcoin new')
@@ -117,6 +116,17 @@ class Test(unittest.TestCase):
with open(config_path) as fs:
settings = json.load(fs)
self.assertTrue(settings['chainclients']['namecoin']['connection_type'] == 'rpc')
logging.info('notorproxy')
testargs = ['basicswap-prepare', '-datadir=' + test_path_plain, '-addcoin=firo', '--usetorproxy', '--notorproxy']
with patch('sys.stderr', new=StringIO()) as fake_stderr:
with patch.object(sys, 'argv', testargs):
with self.assertRaises(SystemExit) as cm:
prepareSystem.main()
self.assertEqual(cm.exception.code, 1)
self.assertTrue('--usetorproxy and --notorproxy together' in fake_stderr.getvalue())
finally:
del prepareSystem

View File

@@ -553,6 +553,40 @@ class Test(unittest.TestCase):
rv_stdout = result.stdout.decode().split('\n')
'''
def test_error_messages(self):
waitForServer(self.delay_event, UI_PORT + 0)
waitForServer(self.delay_event, UI_PORT + 1)
# Reset test
clear_offers(self.delay_event, 0)
delete_file(self.node0_statefile)
delete_file(self.node1_statefile)
wait_for_offers(self.delay_event, 1, 0)
node0_test1_config = {
'offers': [
{
'name': 'offer should fail',
'coin_from': 'Particl',
'coin_to': 'XMR',
'amount': 20,
'minrate': 0.05,
'ratetweakpercent': 50000000,
'amount_variable': True,
'address': -1,
'min_coin_from_amt': 20,
'max_coin_to_amt': -1
}
],
}
with open(self.node0_configfile, 'w') as fp:
json.dump(node0_test1_config, fp, indent=4)
logging.info('Test that an offer is created')
result = subprocess.run(self.node0_args, stdout=subprocess.PIPE)
rv_stdout = result.stdout.decode().split('\n')
assert (count_lines_with(rv_stdout, 'Error: Server failed to create offer: To amount above max') == 1)
def test_bid_tracking(self):
waitForServer(self.delay_event, UI_PORT + 0)

View File

@@ -132,12 +132,12 @@ class Test(TestBase):
callbtcnoderpc(2, 'generatetoaddress', [num_blocks, self.btc_addr])
num_blocks = 431
self.ltc_addr = callltcnoderpc(1, 'getnewaddress', ['mining_addr', 'bech32'])
self.ltc_addr = callltcnoderpc(1, 'getnewaddress', ['mining_addr', 'bech32'], wallet='wallet.dat')
logging.info('Mining %d Litecoin blocks to %s', num_blocks, self.ltc_addr)
callltcnoderpc(1, 'generatetoaddress', [num_blocks, self.ltc_addr])
mweb_addr = callltcnoderpc(1, 'getnewaddress', ['mweb_addr', 'mweb'])
callltcnoderpc(1, 'sendtoaddress', [mweb_addr, 1])
mweb_addr = callltcnoderpc(1, 'getnewaddress', ['mweb_addr', 'mweb'], wallet='mweb')
callltcnoderpc(1, 'sendtoaddress', [mweb_addr, 1], wallet='wallet.dat')
num_blocks = 69
callltcnoderpc(1, 'generatetoaddress', [num_blocks, self.ltc_addr])

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright (c) 2021-2022 tecnovert
# Copyright (c) 2021-2024 tecnovert
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -38,6 +38,7 @@ from basicswap.rpc import (
from tests.basicswap.common import (
BASE_RPC_PORT,
BTC_BASE_RPC_PORT,
LTC_BASE_RPC_PORT,
)
from tests.basicswap.util import (
make_boolean,
@@ -59,8 +60,9 @@ 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))
XMR_BASE_RPC_PORT = int(os.getenv('XMR_BASE_RPC_PORT', XMR_BASE_RPC_PORT))
TEST_COINS_LIST = os.getenv('TEST_COINS_LIST', 'bitcoin,monero')
NUM_NODES = int(os.getenv('NUM_NODES', 3))
EXTRA_CONFIG_JSON = json.loads(os.getenv('EXTRA_CONFIG_JSON', '{}'))
@@ -81,6 +83,11 @@ def callbtcrpc(node_id, method, params=[], wallet=None, base_rpc_port=BITCOIN_RP
return callrpc(base_rpc_port + node_id, auth, method, params, wallet)
def callltcrpc(node_id, method, params=[], wallet=None, base_rpc_port=LITECOIN_RPC_PORT_BASE + PORT_OFS):
auth = 'test_ltc_{0}:test_ltc_pwd_{0}'.format(node_id)
return callrpc(base_rpc_port + node_id, auth, method, params, wallet)
def updateThread(cls):
while not cls.delay_event.is_set():
try:
@@ -129,7 +136,7 @@ class Test(unittest.TestCase):
logging.info(f'Continuing with existing directory: {test_path}')
else:
logging.info('Preparing %d nodes.', NUM_NODES)
prepare_nodes(NUM_NODES, 'bitcoin,monero', True, {'min_sequence_lock_seconds': 60}, PORT_OFS)
prepare_nodes(NUM_NODES, TEST_COINS_LIST, True, {'min_sequence_lock_seconds': 60}, PORT_OFS)
signal.signal(signal.SIGINT, lambda signal, frame: cls.signal_handler(cls, signal, frame))
@@ -167,11 +174,28 @@ class Test(unittest.TestCase):
logging.info('XMR blocks: %d', callrpc_xmr(XMR_BASE_RPC_PORT + 1, 'get_block_count', auth=xmr_auth)['count'])
self.btc_addr = callbtcrpc(0, 'getnewaddress', ['mining_addr', 'bech32'])
num_blocks = 500 # Mine enough to activate segwit
if callbtcrpc(0, 'getblockchaininfo')['blocks'] < num_blocks:
num_blocks: int = 500 # Mine enough to activate segwit
if callbtcrpc(0, 'getblockcount') < num_blocks:
logging.info('Mining %d Bitcoin blocks to %s', num_blocks, self.btc_addr)
callbtcrpc(0, 'generatetoaddress', [num_blocks, self.btc_addr])
logging.info('BTC blocks: %d', callbtcrpc(0, 'getblockchaininfo')['blocks'])
logging.info('BTC blocks: %d', callbtcrpc(0, 'getblockcount'))
if 'litecoin' in TEST_COINS_LIST:
self.ltc_addr = callltcrpc(0, 'getnewaddress', ['mining_addr'], wallet='wallet.dat')
num_blocks: int = 431
have_blocks: int = callltcrpc(0, 'getblockcount')
if have_blocks < 500:
logging.info('Mining %d Litecoin blocks to %s', num_blocks, self.ltc_addr)
callltcrpc(0, 'generatetoaddress', [num_blocks - have_blocks, self.ltc_addr], wallet='wallet.dat')
# https://github.com/litecoin-project/litecoin/issues/807
# Block 432 is when MWEB activates. It requires a peg-in. You'll need to generate an mweb address and send some coins to it. Then it will allow you to mine the next block.
mweb_addr = callltcrpc(0, 'getnewaddress', ['mweb_addr', 'mweb'], wallet='mweb')
callltcrpc(0, 'sendtoaddress', [mweb_addr, 1.0], wallet='wallet.dat')
num_blocks = 69
have_blocks: int = callltcrpc(0, 'getblockcount')
callltcrpc(0, 'generatetoaddress', [500 - have_blocks, self.ltc_addr], wallet='wallet.dat')
# Lower output split threshold for more stakeable outputs
for i in range(NUM_NODES):

View File

@@ -0,0 +1,2 @@
python tests/basicswap/extended/test_xmr_persistent.py
python tests/basicswap/selenium/test_wallets.py

View File

@@ -5,35 +5,22 @@
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
"""
cd /tmp
wget -4 https://chromedriver.storage.googleapis.com/114.0.5735.90/chromedriver_linux64.zip
7z x chromedriver_linux64.zip
sudo mv chromedriver /opt/chromedriver114
python tests/basicswap/extended/test_xmr_persistent.py
python tests/basicswap/selenium/test_offer.py
"""
import json
import time
from urllib.request import urlopen
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import Select
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from util import get_driver
def test_html():
def test_offer(driver):
node1_url = 'http://localhost:12701'
node2_url = 'http://localhost:12702'
driver = webdriver.Chrome(service=Service('/opt/chromedriver114'))
driver.get(node1_url + '/newoffer')
time.sleep(1)
@@ -99,10 +86,16 @@ def test_html():
assert (offer4_json['coin_from'] == 'Particl')
assert (offer4_json['coin_to'] == 'Monero')
driver.close()
print('Test Passed!')
print('Done.')
def run_tests():
driver = get_driver()
try:
test_offer(driver)
finally:
driver.close()
if __name__ == '__main__':
test_html()
run_tests()

View File

@@ -1,27 +1,26 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright (c) 2022 tecnovert
# Copyright (c) 2022-2023 tecnovert
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import json
import time
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.support.select import Select
from selenium.webdriver.support import expected_conditions as EC
from util import get_driver
from basicswap.ui.page_offers import default_chart_api_key
def test_html():
def test_settings(driver):
base_url = 'http://localhost:12701'
node2_url = 'http://localhost:12702'
driver = webdriver.Chrome(service=Service('/opt/chromedriver96'))
url = base_url + '/settings'
driver.get(url)
driver.find_element(By.ID, 'general-tab').click()
@@ -48,8 +47,8 @@ def test_html():
btn_apply_general.click()
time.sleep(1)
settings_path = '/tmp/test_persistent/client0/basicswap.json'
with open(settings_path) as fs:
settings_path_0 = '/tmp/test_persistent/client0/basicswap.json'
with open(settings_path_0) as fs:
settings = json.load(fs)
assert (settings['debug'] is False)
@@ -65,21 +64,21 @@ def test_html():
difficult_text = '`~!@#$%^&*()-_=+[{}]\\|;:\'",<>./? '
el = driver.find_element(By.NAME, 'chartapikey')
el.clear()
el.send_keys(difficult_text)
btn_apply_chart = wait.until(EC.element_to_be_clickable((By.NAME, 'apply_chart')))
btn_apply_chart.click()
time.sleep(1)
settings_path = '/tmp/test_persistent/client0/basicswap.json'
with open(settings_path) as fs:
with open(settings_path_0) as fs:
settings = json.load(fs)
assert (settings['show_chart'] is False)
chart_api_key = bytes.fromhex(settings.get('chart_api_key_enc', '')).decode('utf-8')
assert (chart_api_key == difficult_text)
hex_text = 'cd7600e7b5fdd99c6f900673ff0ee8f64d6d4219a4bb87191ad4a2e3fc65d7f4'
hex_text = default_chart_api_key
el = driver.find_element(By.NAME, 'chartapikey')
el.clear()
el.send_keys(hex_text)
@@ -90,16 +89,55 @@ def test_html():
el = driver.find_element(By.NAME, 'chartapikey')
assert el.get_property('value') == hex_text
settings_path = '/tmp/test_persistent/client0/basicswap.json'
with open(settings_path) as fs:
with open(settings_path_0) as fs:
settings = json.load(fs)
assert (settings.get('chart_api_key') == hex_text)
driver.close()
# Apply XMR settings with blank nodes list
driver.find_element(By.ID, 'coins-tab').click()
btn_apply_monero = wait.until(EC.element_to_be_clickable((By.NAME, 'apply_monero')))
el = driver.find_element(By.NAME, 'remotedaemonurls_monero')
el.clear()
btn_apply_monero.click()
time.sleep(1)
print('Done.')
with open(settings_path_0) as fs:
settings = json.load(fs)
assert ('remote_daemon_urls' not in settings['chainclients']['monero'])
btn_apply_monero = wait.until(EC.element_to_be_clickable((By.NAME, 'apply_monero')))
el = driver.find_element(By.NAME, 'remotedaemonurls_monero')
el.clear()
el.send_keys('node.xmr.to:18081\nnode1.xmr.to:18082')
btn_apply_monero.click()
time.sleep(1)
with open(settings_path_0) as fs:
settings = json.load(fs)
remotedaemonurls = settings['chainclients']['monero']['remote_daemon_urls']
assert (len(remotedaemonurls) == 2)
btn_apply_monero = wait.until(EC.element_to_be_clickable((By.NAME, 'apply_monero')))
el = driver.find_element(By.NAME, 'remotedaemonurls_monero')
el.clear()
btn_apply_monero.click()
time.sleep(1)
with open(settings_path_0) as fs:
settings = json.load(fs)
assert ('remote_daemon_urls' not in settings['chainclients']['monero'])
print('Test Passed!')
def run_tests():
driver = get_driver()
try:
test_settings(driver)
finally:
driver.close()
if __name__ == '__main__':
test_html()
run_tests()

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright (c) 2022 tecnovert
# Copyright (c) 2022-2023 tecnovert
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -11,11 +11,10 @@ from tests.basicswap.util import (
read_json_api,
)
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from util import get_driver
def test_html(driver):
def test_swap_dir(driver):
node_1_port = 12701
node_2_port = 12702
node1_url = f'http://localhost:{node_1_port}'
@@ -58,12 +57,16 @@ def test_html(driver):
raise ValueError('TODO')
print('Done.')
print('Test Passed!')
def run_tests():
driver = get_driver()
try:
test_swap_dir(driver)
finally:
driver.close()
if __name__ == '__main__':
driver = webdriver.Chrome(service=Service('/opt/chromedriver96'))
try:
test_html(driver)
finally:
driver.close()
run_tests()

View File

@@ -1,36 +1,24 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright (c) 2022-2023 tecnovert
# Copyright (c) 2022-2024 tecnovert
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
"""
cd /tmp
wget -4 https://chromedriver.storage.googleapis.com/114.0.5735.90/chromedriver_linux64.zip
7z x chromedriver_linux64.zip
sudo mv chromedriver /opt/chromedriver114
python tests/basicswap/extended/test_xmr_persistent.py
python tests/basicswap/selenium/test_wallets.py
"""
import json
import time
from urllib.request import urlopen
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.service import Service
from urllib.request import urlopen
from basicswap.util import dumpje
from util import get_driver
def test_html():
def test_wallets(driver):
base_url = 'http://localhost:12701'
node2_url = 'http://localhost:12702'
driver = webdriver.Chrome(service=Service('/opt/chromedriver114'))
# Check json coins data
coins = json.loads(urlopen(base_url + '/json/coins').read())
part_coin = [f for f in coins if f['ticker'] == 'PART'][0]
@@ -40,13 +28,13 @@ def test_html():
# Check 404 pages
url = base_url + '/unknown'
driver.get(url)
p1 = driver.find_element(By.TAG_NAME, 'p')
assert ('404' in p1.text)
p1 = driver.find_element(By.TAG_NAME, 'body')
assert ('Error 404' in p1.text)
url = base_url + '/static/nothing.png'
driver.get(url)
p1 = driver.find_element(By.TAG_NAME, 'p')
assert ('404' in p1.text)
p1 = driver.find_element(By.TAG_NAME, 'body')
assert ('Error 404' in p1.text)
url = base_url + '/wallet'
driver.get(url)
@@ -81,15 +69,65 @@ def test_html():
driver.find_element(By.NAME, f'withdraw_{part_id}').click()
driver.switch_to.alert.accept()
time.sleep(1)
elements = driver.find_elements(By.CLASS_NAME, "infomsg")
elements = driver.find_elements(By.CLASS_NAME, 'infomsg')
assert (len(elements) == 1)
e = elements[0]
assert ('Withdrew 10 rtPART (plain to plain) to address' in e.text)
driver.close()
print('Locking UTXO')
driver.get(base_url + '/rpc')
el = driver.find_element(By.NAME, 'coin_type')
for option in el.find_elements(By.TAG_NAME, 'option'):
if option.text == 'Particl':
option.click()
break
driver.find_element(By.NAME, 'cmd').send_keys('listunspent')
driver.find_element(By.NAME, 'apply').click()
time.sleep(1)
print('Done.')
text_value = driver.find_element(By.NAME, 'result').text
utxos = json.loads(text_value.split('\n', 1)[1])
lock_utxos = [{'txid': utxos[0]['txid'], 'vout': utxos[0]['vout']}]
driver.find_element(By.NAME, 'cmd').send_keys('lockunspent false "{}"'.format(dumpje(lock_utxos)))
driver.find_element(By.NAME, 'apply').click()
print('Check for locked UTXO count')
driver.get(base_url + '/wallet/PART')
found = False
for i in range(5):
try:
el = driver.find_element(By.ID, 'locked_utxos')
found = True
break
except Exception:
continue
driver.find_element(By.ID, 'refresh').click()
time.sleep(2)
found = True
assert (found)
driver.refresh()
print('Unlocking UTXO')
driver.get(base_url + '/rpc')
el = driver.find_element(By.NAME, 'coin_type')
for option in el.find_elements(By.TAG_NAME, 'option'):
if option.text == 'Particl':
option.click()
break
driver.find_element(By.NAME, 'cmd').send_keys('lockunspent true "{}"'.format(dumpje(lock_utxos)))
driver.find_element(By.NAME, 'apply').click()
print('Test Passed!')
def run_tests():
driver = get_driver()
try:
test_wallets(driver)
finally:
driver.close()
if __name__ == '__main__':
test_html()
run_tests()

View File

@@ -0,0 +1,14 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright (c) 2023 tecnovert
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
from selenium.webdriver import Firefox
def get_driver():
# driver = Chrome() # 2023-11: Hangs here
driver = Firefox()
return driver

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright (c) 2021-2023 tecnovert
# Copyright (c) 2021-2024 tecnovert
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -9,6 +9,9 @@ import random
import logging
import unittest
from basicswap.db import (
Concepts,
)
from basicswap.basicswap import (
Coins,
SwapTypes,
@@ -17,6 +20,7 @@ from basicswap.basicswap import (
)
from basicswap.basicswap_util import (
TxLockTypes,
EventLogTypes,
)
from basicswap.util import (
make_int,
@@ -27,7 +31,9 @@ from tests.basicswap.util import (
read_json_api,
)
from tests.basicswap.common import (
abandon_all_swaps,
wait_for_bid,
wait_for_event,
wait_for_offer,
wait_for_balance,
wait_for_unspent,
@@ -59,6 +65,17 @@ class TestFunctions(BaseTest):
node_a_id = 0
node_b_id = 1
node_c_id = 2
def callnoderpc(self, method, params=[], wallet=None, node_id=0):
return callnoderpc(node_id, method, params, wallet, self.base_rpc_port)
def mineBlock(self, num_blocks=1):
self.callnoderpc('generatetoaddress', [num_blocks, self.btc_addr])
def check_softfork_active(self, feature_name):
deploymentinfo = self.callnoderpc('getdeploymentinfo')
assert (deploymentinfo['deployments'][feature_name]['active'] is True)
def getBalance(self, js_wallets, coin) -> float:
if coin == Coins.PART_BLIND:
@@ -79,12 +96,6 @@ class TestFunctions(BaseTest):
return float(js_wallets[coin_ticker][balance_type]) + float(js_wallets[coin_ticker][unconfirmed_name])
def callnoderpc(self, method, params=[], wallet=None, node_id=0):
return callnoderpc(node_id, method, params, wallet, self.base_rpc_port)
def mineBlock(self, num_blocks=1):
self.callnoderpc('generatetoaddress', [num_blocks, self.btc_addr])
def prepare_balance(self, coin, amount: float, port_target_node: int, port_take_from_node: int, test_balance: bool = True) -> None:
delay_iterations = 100 if coin == Coins.NAV else 20
delay_time = 5 if coin == Coins.NAV else 3
@@ -149,8 +160,8 @@ class TestFunctions(BaseTest):
js_1 = read_json_api(1800 + id_bidder, 'wallets')
node1_from_before: float = self.getBalance(js_1, coin_from)
node0_sent_messages_before: int = ci_part0.rpc_callback('smsgoutbox', ['count',])['num_messages']
node1_sent_messages_before: int = ci_part1.rpc_callback('smsgoutbox', ['count',])['num_messages']
node0_sent_messages_before: int = ci_part0.rpc('smsgoutbox', ['count',])['num_messages']
node1_sent_messages_before: int = ci_part1.rpc('smsgoutbox', ['count',])['num_messages']
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)
@@ -162,9 +173,6 @@ class TestFunctions(BaseTest):
post_json = {'with_extra_info': True}
offer0 = read_json_api(1800 + id_offerer, f'offers/{offer_id.hex()}', post_json)[0]
offer1 = read_json_api(1800 + id_offerer, f'offers/{offer_id.hex()}', post_json)[0]
from basicswap.util import dumpj
logging.info('offer0 {} '.format(dumpj(offer0)))
logging.info('offer1 {} '.format(dumpj(offer1)))
assert ('lock_time_1' in offer0)
assert ('lock_time_1' in offer1)
@@ -189,9 +197,7 @@ class TestFunctions(BaseTest):
found: bool = False
bids0 = read_json_api(1800 + id_offerer, 'bids')
logging.info('bids0 {} '.format(bids0))
for bid in bids0:
logging.info('bid {} '.format(bid))
if bid['bid_id'] != bid_id.hex():
continue
assert (bid['amount_from'] == bid1['amt_from'])
@@ -224,8 +230,8 @@ class TestFunctions(BaseTest):
if False: # TODO: set stakeaddress and xmr rewards to non wallet addresses
assert (node1_to_after < node1_to_before - amount_to_float)
node0_sent_messages_after: int = ci_part0.rpc_callback('smsgoutbox', ['count',])['num_messages']
node1_sent_messages_after: int = ci_part1.rpc_callback('smsgoutbox', ['count',])['num_messages']
node0_sent_messages_after: int = ci_part0.rpc('smsgoutbox', ['count',])['num_messages']
node1_sent_messages_after: int = ci_part1.rpc('smsgoutbox', ['count',])['num_messages']
node0_sent_messages: int = node0_sent_messages_after - node0_sent_messages_before
node1_sent_messages: int = node1_sent_messages_after - node1_sent_messages_before
split_msgs: int = 2 if (ci_from.curve_type() != Curves.secp256k1 or ci_to.curve_type() != Curves.secp256k1) else 0
@@ -235,8 +241,6 @@ class TestFunctions(BaseTest):
post_json = {'show_extra': True}
bid0 = read_json_api(1800 + id_offerer, f'bids/{bid_id.hex()}', post_json)
bid1 = read_json_api(1800 + id_bidder, f'bids/{bid_id.hex()}', post_json)
logging.info('bid0 {} '.format(dumpj(bid0)))
logging.info('bid1 {} '.format(dumpj(bid1)))
chain_a_lock_txid = None
chain_b_lock_txid = None
@@ -413,20 +417,19 @@ class TestFunctions(BaseTest):
def do_test_05_self_bid(self, coin_from, coin_to):
logging.info('---------- Test {} to {} same client'.format(coin_from.name, coin_to.name))
id_offerer: int = self.node_a_id
id_bidder: int = self.node_b_id
id_both: int = self.node_b_id
swap_clients = self.swap_clients
ci_from = swap_clients[id_offerer].ci(coin_from)
ci_to = swap_clients[id_offerer].ci(coin_to)
ci_from = swap_clients[id_both].ci(coin_from)
ci_to = swap_clients[id_both].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[id_bidder].postOffer(coin_from, coin_to, amt_swap, rate_swap, amt_swap, SwapTypes.XMR_SWAP, auto_accept_bids=True)
bid_id = swap_clients[id_bidder].postXmrBid(offer_id, amt_swap)
offer_id = swap_clients[id_both].postOffer(coin_from, coin_to, amt_swap, rate_swap, amt_swap, SwapTypes.XMR_SWAP, auto_accept_bids=True)
bid_id = swap_clients[id_both].postXmrBid(offer_id, amt_swap)
wait_for_bid(test_delay_event, swap_clients[id_bidder], bid_id, BidStates.SWAP_COMPLETED, wait_for=(self.extra_wait_time + 180))
wait_for_bid(test_delay_event, swap_clients[id_both], bid_id, BidStates.SWAP_COMPLETED, wait_for=(self.extra_wait_time + 180))
class BasicSwapTest(TestFunctions):
@@ -434,21 +437,22 @@ class BasicSwapTest(TestFunctions):
def test_001_nested_segwit(self):
# p2sh-p2wpkh
logging.info('---------- Test {} p2sh nested segwit'.format(self.test_coin_from.name))
ci = self.swap_clients[0].ci(self.test_coin_from)
addr_p2sh_segwit = self.callnoderpc('getnewaddress', ['segwit test', 'p2sh-segwit'])
addr_info = self.callnoderpc('getaddressinfo', [addr_p2sh_segwit, ])
addr_p2sh_segwit = ci.rpc_wallet('getnewaddress', ['segwit test', 'p2sh-segwit'])
addr_info = ci.rpc_wallet('getaddressinfo', [addr_p2sh_segwit, ])
assert addr_info['script'] == 'witness_v0_keyhash'
txid = self.callnoderpc('sendtoaddress', [addr_p2sh_segwit, 1.0])
txid = ci.rpc_wallet('sendtoaddress', [addr_p2sh_segwit, 1.0])
assert len(txid) == 64
self.mineBlock()
ro = self.callnoderpc('scantxoutset', ['start', ['addr({})'.format(addr_p2sh_segwit)]])
ro = ci.rpc('scantxoutset', ['start', ['addr({})'.format(addr_p2sh_segwit)]])
assert (len(ro['unspents']) == 1)
assert (ro['unspents'][0]['txid'] == txid)
tx_wallet = self.callnoderpc('gettransaction', [txid, ])['hex']
tx = self.callnoderpc('decoderawtransaction', [tx_wallet, ])
tx_wallet = ci.rpc_wallet('gettransaction', [txid, ])['hex']
tx = ci.rpc('decoderawtransaction', [tx_wallet, ])
prevout_n = -1
for txo in tx['vout']:
@@ -457,14 +461,14 @@ class BasicSwapTest(TestFunctions):
break
assert prevout_n > -1
tx_funded = self.callnoderpc('createrawtransaction', [[{'txid': txid, 'vout': prevout_n}], {addr_p2sh_segwit: 0.99}])
tx_signed = self.callnoderpc('signrawtransactionwithwallet', [tx_funded, ])['hex']
tx_funded_decoded = self.callnoderpc('decoderawtransaction', [tx_funded, ])
tx_signed_decoded = self.callnoderpc('decoderawtransaction', [tx_signed, ])
tx_funded = ci.rpc('createrawtransaction', [[{'txid': txid, 'vout': prevout_n}], {addr_p2sh_segwit: 0.99}])
tx_signed = ci.rpc_wallet('signrawtransactionwithwallet', [tx_funded, ])['hex']
tx_funded_decoded = ci.rpc('decoderawtransaction', [tx_funded, ])
tx_signed_decoded = ci.rpc('decoderawtransaction', [tx_signed, ])
assert tx_funded_decoded['txid'] != tx_signed_decoded['txid']
# Add scriptsig for txids to match
addr_p2sh_segwit_info = self.callnoderpc('getaddressinfo', [addr_p2sh_segwit, ])
addr_p2sh_segwit_info = ci.rpc_wallet('getaddressinfo', [addr_p2sh_segwit, ])
decoded_tx = FromHex(CTransaction(), tx_funded)
decoded_tx.vin[0].scriptSig = bytes.fromhex('16' + addr_p2sh_segwit_info['hex'])
txid_with_scriptsig = decoded_tx.rehash()
@@ -473,18 +477,19 @@ class BasicSwapTest(TestFunctions):
def test_002_native_segwit(self):
# p2wpkh
logging.info('---------- Test {} p2sh native segwit'.format(self.test_coin_from.name))
ci = self.swap_clients[0].ci(self.test_coin_from)
addr_segwit = self.callnoderpc('getnewaddress', ['segwit test', 'bech32'])
addr_info = self.callnoderpc('getaddressinfo', [addr_segwit, ])
addr_segwit = ci.rpc_wallet('getnewaddress', ['segwit test', 'bech32'])
addr_info = ci.rpc_wallet('getaddressinfo', [addr_segwit, ])
assert addr_info['iswitness'] is True
txid = self.callnoderpc('sendtoaddress', [addr_segwit, 1.0])
txid = ci.rpc_wallet('sendtoaddress', [addr_segwit, 1.0])
assert len(txid) == 64
tx_wallet = self.callnoderpc('gettransaction', [txid, ])['hex']
tx = self.callnoderpc('decoderawtransaction', [tx_wallet, ])
tx_wallet = ci.rpc_wallet('gettransaction', [txid, ])['hex']
tx = ci.rpc('decoderawtransaction', [tx_wallet, ])
self.mineBlock()
ro = self.callnoderpc('scantxoutset', ['start', ['addr({})'.format(addr_segwit)]])
ro = ci.rpc('scantxoutset', ['start', ['addr({})'.format(addr_segwit)]])
assert (len(ro['unspents']) == 1)
assert (ro['unspents'][0]['txid'] == txid)
@@ -495,16 +500,18 @@ class BasicSwapTest(TestFunctions):
break
assert prevout_n > -1
tx_funded = self.callnoderpc('createrawtransaction', [[{'txid': txid, 'vout': prevout_n}], {addr_segwit: 0.99}])
tx_signed = self.callnoderpc('signrawtransactionwithwallet', [tx_funded, ])['hex']
tx_funded_decoded = self.callnoderpc('decoderawtransaction', [tx_funded, ])
tx_signed_decoded = self.callnoderpc('decoderawtransaction', [tx_signed, ])
tx_funded = ci.rpc('createrawtransaction', [[{'txid': txid, 'vout': prevout_n}], {addr_segwit: 0.99}])
tx_signed = ci.rpc_wallet('signrawtransactionwithwallet', [tx_funded, ])['hex']
tx_funded_decoded = ci.rpc('decoderawtransaction', [tx_funded, ])
tx_signed_decoded = ci.rpc('decoderawtransaction', [tx_signed, ])
assert tx_funded_decoded['txid'] == tx_signed_decoded['txid']
def test_003_cltv(self):
logging.info('---------- Test {} cltv'.format(self.test_coin_from.name))
ci = self.swap_clients[0].ci(self.test_coin_from)
self.check_softfork_active('bip65')
chain_height = self.callnoderpc('getblockcount')
script = CScript([chain_height + 3, OP_CHECKLOCKTIMEVERIFY, ])
@@ -513,12 +520,12 @@ class BasicSwapTest(TestFunctions):
tx.nVersion = ci.txVersion()
tx.vout.append(ci.txoType()(ci.make_int(1.1), script_dest))
tx_hex = ToHex(tx)
tx_funded = self.callnoderpc('fundrawtransaction', [tx_hex])
tx_funded = ci.rpc_wallet('fundrawtransaction', [tx_hex])
utxo_pos = 0 if tx_funded['changepos'] == 1 else 1
tx_signed = self.callnoderpc('signrawtransactionwithwallet', [tx_funded['hex'], ])['hex']
txid = self.callnoderpc('sendrawtransaction', [tx_signed, ])
tx_signed = ci.rpc_wallet('signrawtransactionwithwallet', [tx_funded['hex'], ])['hex']
txid = ci.rpc('sendrawtransaction', [tx_signed, ])
addr_out = self.callnoderpc('getnewaddress', ['csv test', 'bech32'])
addr_out = ci.rpc_wallet('getnewaddress', ['cltv test', 'bech32'])
pkh = ci.decodeSegwitAddress(addr_out)
script_out = ci.getScriptForPubkeyHash(pkh)
@@ -530,28 +537,46 @@ class BasicSwapTest(TestFunctions):
tx_spend.wit.vtxinwit.append(CTxInWitness())
tx_spend.wit.vtxinwit[0].scriptWitness.stack = [script, ]
tx_spend_hex = ToHex(tx_spend)
tx_spend.nLockTime = chain_height + 2
tx_spend_invalid_hex = ToHex(tx_spend)
for tx_hex in [tx_spend_invalid_hex, tx_spend_hex]:
try:
txid = self.callnoderpc('sendrawtransaction', [tx_hex, ])
except Exception as e:
assert ('non-final' in str(e))
else:
assert False, 'Should fail'
self.mineBlock(5)
try:
txid = self.callnoderpc('sendrawtransaction', [tx_spend_hex, ])
txid = ci.rpc('sendrawtransaction', [tx_spend_invalid_hex, ])
except Exception as e:
assert ('non-final' in str(e))
assert ('Locktime requirement not satisfied' in str(e))
else:
assert False, 'Should fail'
self.mineBlock(5)
txid = self.callnoderpc('sendrawtransaction', [tx_spend_hex, ])
txid = ci.rpc('sendrawtransaction', [tx_spend_hex, ])
self.mineBlock()
ro = self.callnoderpc('listreceivedbyaddress', [0, ])
ro = ci.rpc_wallet('listreceivedbyaddress', [0, ])
sum_addr = 0
for entry in ro:
if entry['address'] == addr_out:
sum_addr += entry['amount']
assert (sum_addr == 1.0999)
# Ensure tx was mined
tx_wallet = ci.rpc_wallet('gettransaction', [txid, ])
assert (len(tx_wallet['blockhash']) == 64)
def test_004_csv(self):
logging.info('---------- Test {} csv'.format(self.test_coin_from.name))
swap_clients = self.swap_clients
ci = self.swap_clients[0].ci(self.test_coin_from)
self.check_softfork_active('csv')
script = CScript([3, OP_CHECKSEQUENCEVERIFY, ])
script_dest = ci.getScriptDest(script)
@@ -559,17 +584,17 @@ class BasicSwapTest(TestFunctions):
tx.nVersion = ci.txVersion()
tx.vout.append(ci.txoType()(ci.make_int(1.1), script_dest))
tx_hex = ToHex(tx)
tx_funded = self.callnoderpc('fundrawtransaction', [tx_hex])
tx_funded = ci.rpc_wallet('fundrawtransaction', [tx_hex])
utxo_pos = 0 if tx_funded['changepos'] == 1 else 1
tx_signed = self.callnoderpc('signrawtransactionwithwallet', [tx_funded['hex'], ])['hex']
txid = self.callnoderpc('sendrawtransaction', [tx_signed, ])
tx_signed = ci.rpc_wallet('signrawtransactionwithwallet', [tx_funded['hex'], ])['hex']
txid = ci.rpc('sendrawtransaction', [tx_signed, ])
addr_out = self.callnoderpc('getnewaddress', ['csv test', 'bech32'])
addr_out = ci.rpc_wallet('getnewaddress', ['csv test', 'bech32'])
pkh = ci.decodeSegwitAddress(addr_out)
script_out = ci.getScriptForPubkeyHash(pkh)
# Double check output type
prev_tx = self.callnoderpc('decoderawtransaction', [tx_signed, ])
prev_tx = ci.rpc('decoderawtransaction', [tx_signed, ])
assert (prev_tx['vout'][utxo_pos]['scriptPubKey']['type'] == 'witness_v0_scripthash')
tx_spend = CTransaction()
@@ -581,16 +606,16 @@ class BasicSwapTest(TestFunctions):
tx_spend.wit.vtxinwit[0].scriptWitness.stack = [script, ]
tx_spend_hex = ToHex(tx_spend)
try:
txid = self.callnoderpc('sendrawtransaction', [tx_spend_hex, ])
txid = ci.rpc('sendrawtransaction', [tx_spend_hex, ])
except Exception as e:
assert ('non-BIP68-final' in str(e))
else:
assert False, 'Should fail'
self.mineBlock(3)
txid = self.callnoderpc('sendrawtransaction', [tx_spend_hex, ])
txid = ci.rpc('sendrawtransaction', [tx_spend_hex, ])
self.mineBlock(1)
ro = self.callnoderpc('listreceivedbyaddress', [0, ])
ro = ci.rpc_wallet('listreceivedbyaddress', [0, ])
sum_addr = 0
for entry in ro:
if entry['address'] == addr_out:
@@ -598,20 +623,22 @@ class BasicSwapTest(TestFunctions):
assert (sum_addr == 1.0999)
# Ensure tx was mined
tx_wallet = self.callnoderpc('gettransaction', [txid, ])
tx_wallet = ci.rpc_wallet('gettransaction', [txid, ])
assert (len(tx_wallet['blockhash']) == 64)
def test_005_watchonly(self):
logging.info('---------- Test {} watchonly'.format(self.test_coin_from.name))
ci = self.swap_clients[0].ci(self.test_coin_from)
ci1 = self.swap_clients[1].ci(self.test_coin_from)
addr = self.callnoderpc('getnewaddress', ['watchonly test', 'bech32'])
ro = self.callnoderpc('importaddress', [addr, '', False], node_id=1)
txid = self.callnoderpc('sendtoaddress', [addr, 1.0])
tx_hex = self.callnoderpc('getrawtransaction', [txid, ])
self.callnoderpc('sendrawtransaction', [tx_hex, ], node_id=1)
ro = self.callnoderpc('gettransaction', [txid, ], node_id=1)
addr = ci.rpc_wallet('getnewaddress', ['watchonly test', 'bech32'])
ro = ci1.rpc_wallet('importaddress', [addr, '', False])
txid = ci.rpc_wallet('sendtoaddress', [addr, 1.0])
tx_hex = ci.rpc('getrawtransaction', [txid, ])
ci1.rpc_wallet('sendrawtransaction', [tx_hex, ])
ro = ci1.rpc_wallet('gettransaction', [txid, ])
assert (ro['txid'] == txid)
balances = self.callnoderpc('getbalances', node_id=1)
balances = ci1.rpc_wallet('getbalances')
assert (balances['watchonly']['trusted'] + balances['watchonly']['untrusted_pending'] >= 1.0)
def test_006_getblock_verbosity(self):
@@ -623,6 +650,7 @@ class BasicSwapTest(TestFunctions):
def test_007_hdwallet(self):
logging.info('---------- Test {} hdwallet'.format(self.test_coin_from.name))
ci = self.swap_clients[0].ci(self.test_coin_from)
test_seed = '8e54a313e6df8918df6d758fafdbf127a115175fdd2238d0e908dd8093c9ac3b'
test_wif = self.swap_clients[0].ci(self.test_coin_from).encodeKey(bytes.fromhex(test_seed))
@@ -637,12 +665,57 @@ class BasicSwapTest(TestFunctions):
self.swap_clients[0].initialiseWallet(Coins.BTC, raise_errors=True)
assert self.swap_clients[0].checkWalletSeed(Coins.BTC) is True
for i in range(1500):
self.callnoderpc('getnewaddress')
ci.rpc_wallet('getnewaddress')
assert self.swap_clients[0].checkWalletSeed(Coins.BTC) is True
rv = read_json_api(1800, 'getcoinseed', {'coin': 'XMR'})
assert (rv['address'] == '47H7UDLzYEsR28BWttxp59SP1UVSxs4VKDJYSfmz7Wd4Fue5VWuoV9x9eejunwzVSmHWN37gBkaAPNf9VD4bTvwQKsBVWyK')
def test_008_gettxout(self):
logging.info('---------- Test {} gettxout'.format(self.test_coin_from.name))
swap_client = self.swap_clients[0]
ci = swap_client.ci(self.test_coin_from)
addr_1 = ci.rpc_wallet('getnewaddress', ['gettxout test 1',])
txid = ci.rpc_wallet('sendtoaddress', [addr_1, 1.0])
assert len(txid) == 64
self.mineBlock()
unspents = ci.rpc_wallet('listunspent', [0, 999999999, [addr_1,]])
assert (len(unspents) == 1)
utxo = unspents[0]
txout = ci.rpc('gettxout', [utxo['txid'], utxo['vout']])
if 'address' in txout['scriptPubKey']:
assert (addr_1 == txout['scriptPubKey']['address'])
else:
assert (addr_1 in txout['scriptPubKey']['addresses'])
# Spend
addr_2 = ci.rpc_wallet('getnewaddress', ['gettxout test 2',])
tx_funded = ci.rpc('createrawtransaction', [[{'txid': utxo['txid'], 'vout': utxo['vout']}], {addr_2: 0.99}])
tx_signed = ci.rpc_wallet('signrawtransactionwithwallet', [tx_funded,])['hex']
ci.rpc('sendrawtransaction', [tx_signed,])
# utxo should be unavailable when spent in the mempool
txout = ci.rpc('gettxout', [utxo['txid'], utxo['vout']])
assert (txout is None)
def test_009_scantxoutset(self):
logging.info('---------- Test {} scantxoutset'.format(self.test_coin_from.name))
ci = self.swap_clients[0].ci(self.test_coin_from)
addr_1 = ci.rpc_wallet('getnewaddress', ['scantxoutset test', ])
txid = ci.rpc_wallet('sendtoaddress', [addr_1, 1.0])
assert len(txid) == 64
self.mineBlock()
ro = ci.rpc('scantxoutset', ['start', ['addr({})'.format(addr_1)]])
assert (len(ro['unspents']) == 1)
assert (ro['unspents'][0]['txid'] == txid)
def test_010_txn_size(self):
logging.info('---------- Test {} txn_size'.format(self.test_coin_from.name))
@@ -653,7 +726,7 @@ class BasicSwapTest(TestFunctions):
amount: int = ci.make_int(random.uniform(0.1, 2.0), r=1)
# Record unspents before createSCLockTx as the used ones will be locked
unspents = self.callnoderpc('listunspent')
unspents = ci.rpc_wallet('listunspent')
# fee_rate is in sats/kvB
fee_rate: int = 1000
@@ -669,10 +742,10 @@ class BasicSwapTest(TestFunctions):
lock_tx = ci.fundSCLockTx(lock_tx, fee_rate)
lock_tx = ci.signTxWithWallet(lock_tx)
unspents_after = self.callnoderpc('listunspent')
unspents_after = ci.rpc_wallet('listunspent')
assert (len(unspents) > len(unspents_after))
tx_decoded = self.callnoderpc('decoderawtransaction', [lock_tx.hex()])
tx_decoded = ci.rpc('decoderawtransaction', [lock_tx.hex()])
txid = tx_decoded['txid']
vsize = tx_decoded['vsize']
@@ -693,8 +766,8 @@ class BasicSwapTest(TestFunctions):
break
fee_value = in_value - out_value
self.callnoderpc('sendrawtransaction', [lock_tx.hex()])
rv = self.callnoderpc('gettransaction', [txid])
ci.rpc('sendrawtransaction', [lock_tx.hex()])
rv = ci.rpc_wallet('gettransaction', [txid])
wallet_tx_fee = -ci.make_int(rv['fee'])
assert (wallet_tx_fee == fee_value)
@@ -706,7 +779,7 @@ class BasicSwapTest(TestFunctions):
lock_spend_tx = ci.createSCLockSpendTx(lock_tx, lock_tx_script, pkh_out, fee_rate, fee_info=fee_info)
vsize_estimated: int = fee_info['vsize']
tx_decoded = self.callnoderpc('decoderawtransaction', [lock_spend_tx.hex()])
tx_decoded = ci.rpc('decoderawtransaction', [lock_spend_tx.hex()])
txid = tx_decoded['txid']
witness_stack = [
@@ -716,11 +789,11 @@ class BasicSwapTest(TestFunctions):
lock_tx_script,
]
lock_spend_tx = ci.setTxSignature(lock_spend_tx, witness_stack)
tx_decoded = self.callnoderpc('decoderawtransaction', [lock_spend_tx.hex()])
tx_decoded = ci.rpc('decoderawtransaction', [lock_spend_tx.hex()])
vsize_actual: int = tx_decoded['vsize']
assert (vsize_actual <= vsize_estimated and vsize_estimated - vsize_actual < 4)
assert (self.callnoderpc('sendrawtransaction', [lock_spend_tx.hex()]) == txid)
assert (ci.rpc('sendrawtransaction', [lock_spend_tx.hex()]) == txid)
expect_vsize: int = ci.xmr_swap_a_lock_spend_tx_vsize()
assert (expect_vsize >= vsize_actual)
@@ -737,7 +810,7 @@ class BasicSwapTest(TestFunctions):
lock_tx_b_spend = ci.getTransaction(lock_tx_b_spend_txid)
if lock_tx_b_spend is None:
lock_tx_b_spend = ci.getWalletTransaction(lock_tx_b_spend_txid)
lock_tx_b_spend_decoded = self.callnoderpc('decoderawtransaction', [lock_tx_b_spend.hex()])
lock_tx_b_spend_decoded = ci.rpc('decoderawtransaction', [lock_tx_b_spend.hex()])
expect_vsize: int = ci.xmr_swap_b_lock_spend_tx_vsize()
assert (expect_vsize >= lock_tx_b_spend_decoded['vsize'])
@@ -757,17 +830,17 @@ class BasicSwapTest(TestFunctions):
tx.nVersion = ci.txVersion()
tx.vout.append(ci.txoType()(ci.make_int(1.1), script_dest))
tx_hex = ToHex(tx)
tx_funded = self.callnoderpc('fundrawtransaction', [tx_hex])
tx_funded = ci.rpc_wallet('fundrawtransaction', [tx_hex])
utxo_pos = 0 if tx_funded['changepos'] == 1 else 1
tx_signed = self.callnoderpc('signrawtransactionwithwallet', [tx_funded['hex'], ])['hex']
txid = self.callnoderpc('sendrawtransaction', [tx_signed, ])
tx_signed = ci.rpc_wallet('signrawtransactionwithwallet', [tx_funded['hex'], ])['hex']
txid = ci.rpc('sendrawtransaction', [tx_signed, ])
addr_out = self.callnoderpc('getnewaddress', ['csv test', 'bech32'])
addr_out = ci.rpc_wallet('getnewaddress', ['csv test', 'bech32'])
pkh = ci.decodeSegwitAddress(addr_out)
script_out = ci.getScriptForPubkeyHash(pkh)
# Double check output type
prev_tx = self.callnoderpc('decoderawtransaction', [tx_signed, ])
prev_tx = ci.rpc('decoderawtransaction', [tx_signed, ])
assert (prev_tx['vout'][utxo_pos]['scriptPubKey']['type'] == 'scripthash')
tx_spend = CTransaction()
@@ -777,9 +850,9 @@ class BasicSwapTest(TestFunctions):
tx_spend.vout.append(ci.txoType()(ci.make_int(1.0999), script_out))
tx_spend_hex = ToHex(tx_spend)
txid = self.callnoderpc('sendrawtransaction', [tx_spend_hex, ])
txid = ci.rpc('sendrawtransaction', [tx_spend_hex, ])
self.mineBlock(1)
ro = self.callnoderpc('listreceivedbyaddress', [0, ])
ro = ci.rpc_wallet('listreceivedbyaddress', [0, ])
sum_addr = 0
for entry in ro:
if entry['address'] == addr_out:
@@ -787,7 +860,7 @@ class BasicSwapTest(TestFunctions):
assert (sum_addr == 1.0999)
# Ensure tx was mined
tx_wallet = self.callnoderpc('gettransaction', [txid, ])
tx_wallet = ci.rpc_wallet('gettransaction', [txid, ])
assert (len(tx_wallet['blockhash']) == 64)
def test_012_p2sh_p2wsh(self):
@@ -804,17 +877,17 @@ class BasicSwapTest(TestFunctions):
tx.nVersion = ci.txVersion()
tx.vout.append(ci.txoType()(ci.make_int(1.1), script_dest))
tx_hex = ToHex(tx)
tx_funded = self.callnoderpc('fundrawtransaction', [tx_hex])
tx_funded = ci.rpc_wallet('fundrawtransaction', [tx_hex])
utxo_pos = 0 if tx_funded['changepos'] == 1 else 1
tx_signed = self.callnoderpc('signrawtransactionwithwallet', [tx_funded['hex'], ])['hex']
txid = self.callnoderpc('sendrawtransaction', [tx_signed, ])
tx_signed = ci.rpc_wallet('signrawtransactionwithwallet', [tx_funded['hex'], ])['hex']
txid = ci.rpc('sendrawtransaction', [tx_signed, ])
addr_out = self.callnoderpc('getnewaddress', ['csv test', 'bech32'])
addr_out = ci.rpc_wallet('getnewaddress', ['csv test', 'bech32'])
pkh = ci.decodeSegwitAddress(addr_out)
script_out = ci.getScriptForPubkeyHash(pkh)
# Double check output type
prev_tx = self.callnoderpc('decoderawtransaction', [tx_signed, ])
prev_tx = ci.rpc('decoderawtransaction', [tx_signed, ])
assert (prev_tx['vout'][utxo_pos]['scriptPubKey']['type'] == 'scripthash')
tx_spend = CTransaction()
@@ -826,9 +899,9 @@ class BasicSwapTest(TestFunctions):
tx_spend.wit.vtxinwit[0].scriptWitness.stack = [script, ]
tx_spend_hex = ToHex(tx_spend)
txid = self.callnoderpc('sendrawtransaction', [tx_spend_hex, ])
txid = ci.rpc('sendrawtransaction', [tx_spend_hex, ])
self.mineBlock(1)
ro = self.callnoderpc('listreceivedbyaddress', [0, ])
ro = ci.rpc_wallet('listreceivedbyaddress', [0, ])
sum_addr = 0
for entry in ro:
if entry['address'] == addr_out:
@@ -836,7 +909,7 @@ class BasicSwapTest(TestFunctions):
assert (sum_addr == 1.0999)
# Ensure tx was mined
tx_wallet = self.callnoderpc('gettransaction', [txid, ])
tx_wallet = ci.rpc_wallet('gettransaction', [txid, ])
assert (len(tx_wallet['blockhash']) == 64)
def test_01_a_full_swap(self):
@@ -926,6 +999,8 @@ class BasicSwapTest(TestFunctions):
self.do_test_05_self_bid(self.test_coin_from, Coins.PART)
def test_05_self_bid_from_part(self):
if not self.has_segwit:
return
self.do_test_05_self_bid(Coins.PART, self.test_coin_from)
def test_05_self_bid_rev(self):
@@ -986,7 +1061,7 @@ class BasicSwapTest(TestFunctions):
# Verify expected inputs were used
bid, _, _, _, _ = swap_clients[2].getXmrBidAndOffer(bid_id)
assert (bid.xmr_a_lock_tx)
wtx = ci.rpc_callback('gettransaction', [bid.xmr_a_lock_tx.txid.hex(),])
wtx = ci.rpc_wallet('gettransaction', [bid.xmr_a_lock_tx.txid.hex(),])
itx_after = ci.describeTx(wtx['hex'])
assert (len(itx_after['vin']) == len(itx_decoded['vin']))
for i, txin in enumerate(itx_decoded['vin']):
@@ -1020,6 +1095,70 @@ class BasicSwapTest(TestFunctions):
swap_clients[0].check_expired_seconds = old_check_expired_seconds
swap_clients[0].setMockTimeOffset(0)
def test_08_insufficient_funds(self):
tla_from = self.test_coin_from.name
logging.info('---------- Test {} Insufficient Funds'.format(tla_from))
swap_clients = self.swap_clients
coin_from = self.test_coin_from
coin_to = Coins.XMR
self.prepare_balance(coin_from, 10.0, 1802, 1800)
id_offerer: int = self.node_c_id
id_bidder: int = self.node_b_id
swap_clients = self.swap_clients
ci_from = swap_clients[id_offerer].ci(coin_from)
ci_to = swap_clients[id_bidder].ci(coin_to)
jsw = read_json_api(1800 + id_offerer, 'wallets')
balance_from_before: float = self.getBalance(jsw, coin_from)
amt_swap: int = ci_from.make_int(balance_from_before, r=1)
rate_swap: int = ci_to.make_int(2.0, r=1)
offer_id = swap_clients[id_offerer].postOffer(coin_from, coin_to, amt_swap, rate_swap, amt_swap, SwapTypes.XMR_SWAP, auto_accept_bids=True)
wait_for_offer(test_delay_event, swap_clients[id_bidder], offer_id)
bid_id = swap_clients[id_bidder].postXmrBid(offer_id, amt_swap)
event = wait_for_event(test_delay_event, swap_clients[id_offerer], Concepts.BID, bid_id, event_type=EventLogTypes.ERROR, wait_for=60)
assert ('Insufficient funds' in event.event_msg)
wait_for_bid(test_delay_event, swap_clients[id_offerer], bid_id, BidStates.BID_RECEIVED, wait_for=20)
def test_08_insufficient_funds_rev(self):
tla_from = self.test_coin_from.name
logging.info('---------- Test {} Insufficient Funds (reverse)'.format(tla_from))
swap_clients = self.swap_clients
coin_from = Coins.XMR
coin_to = self.test_coin_from
self.prepare_balance(coin_to, 10.0, 1802, 1800)
id_offerer: int = self.node_b_id
id_bidder: int = self.node_c_id
swap_clients = self.swap_clients
ci_from = swap_clients[id_offerer].ci(coin_from)
ci_to = swap_clients[id_bidder].ci(coin_to)
jsw = read_json_api(1800 + id_bidder, 'wallets')
balance_to_before: float = self.getBalance(jsw, coin_to)
amt_swap: int = ci_from.make_int(balance_to_before, r=1)
rate_swap: int = ci_to.make_int(1.0, r=1)
amt_swap -= 1
offer_id = swap_clients[id_offerer].postOffer(coin_from, coin_to, amt_swap, rate_swap, amt_swap, SwapTypes.XMR_SWAP, auto_accept_bids=True)
wait_for_offer(test_delay_event, swap_clients[id_bidder], offer_id)
bid_id = swap_clients[id_bidder].postXmrBid(offer_id, amt_swap)
event = wait_for_event(test_delay_event, swap_clients[id_bidder], Concepts.BID, bid_id, event_type=EventLogTypes.ERROR, wait_for=60)
assert ('Insufficient funds' in event.event_msg)
wait_for_bid(test_delay_event, swap_clients[id_bidder], bid_id, BidStates.BID_ERROR, sent=True, wait_for=20)
class TestBTC(BasicSwapTest):
__test__ = True
@@ -1060,6 +1199,8 @@ class TestBTC(BasicSwapTest):
assert (jsw['locked'] is False)
def test_01_full_swap(self):
abandon_all_swaps(test_delay_event, self.swap_clients[0])
wait_for_none_active(test_delay_event, 1800)
js_0 = read_json_api(1800, 'wallets')
if not js_0['PART']['encrypted']:
read_json_api(1800, 'setpassword', {'oldpassword': '', 'newpassword': 'notapassword123'})

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright (c) 2021-2022 tecnovert
# Copyright (c) 2021-2024 tecnovert
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -24,9 +24,11 @@ from tests.basicswap.common import (
wait_for_bid,
wait_for_offer,
wait_for_in_progress,
TEST_HTTP_PORT,
LTC_BASE_RPC_PORT,
)
from .test_btc_xmr import BasicSwapTest, test_delay_event
from .test_xmr import pause_event
logger = logging.getLogger()
@@ -40,6 +42,10 @@ class TestLTC(BasicSwapTest):
def mineBlock(self, num_blocks=1):
self.callnoderpc('generatetoaddress', [num_blocks, self.ltc_addr])
def check_softfork_active(self, feature_name):
deploymentinfo = self.callnoderpc('getblockchaininfo')
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('Skipped')
@@ -47,17 +53,18 @@ class TestLTC(BasicSwapTest):
def test_002_native_segwit(self):
logging.info('---------- Test {} p2sh native segwit'.format(self.test_coin_from.name))
addr_segwit = self.callnoderpc('getnewaddress', ['segwit test', 'bech32'])
addr_info = self.callnoderpc('getaddressinfo', [addr_segwit, ])
ci = self.swap_clients[0].ci(self.test_coin_from)
addr_segwit = ci.rpc_wallet('getnewaddress', ['segwit test', 'bech32'])
addr_info = ci.rpc_wallet('getaddressinfo', [addr_segwit, ])
assert addr_info['iswitness'] is True
txid = self.callnoderpc('sendtoaddress', [addr_segwit, 1.0])
txid = ci.rpc_wallet('sendtoaddress', [addr_segwit, 1.0])
assert len(txid) == 64
tx_wallet = self.callnoderpc('gettransaction', [txid, ])['hex']
tx = self.callnoderpc('decoderawtransaction', [tx_wallet, ])
tx_wallet = ci.rpc_wallet('gettransaction', [txid, ])['hex']
tx = ci.rpc('decoderawtransaction', [tx_wallet, ])
self.mineBlock()
ro = self.callnoderpc('scantxoutset', ['start', ['addr({})'.format(addr_segwit)]])
ro = ci.rpc('scantxoutset', ['start', ['addr({})'.format(addr_segwit)]])
assert (len(ro['unspents']) == 1)
assert (ro['unspents'][0]['txid'] == txid)
@@ -68,10 +75,10 @@ class TestLTC(BasicSwapTest):
break
assert prevout_n > -1
tx_funded = self.callnoderpc('createrawtransaction', [[{'txid': txid, 'vout': prevout_n}], {addr_segwit: 0.99}])
tx_signed = self.callnoderpc('signrawtransactionwithwallet', [tx_funded, ])['hex']
tx_funded_decoded = self.callnoderpc('decoderawtransaction', [tx_funded, ])
tx_signed_decoded = self.callnoderpc('decoderawtransaction', [tx_signed, ])
tx_funded = ci.rpc('createrawtransaction', [[{'txid': txid, 'vout': prevout_n}], {addr_segwit: 0.99}])
tx_signed = ci.rpc_wallet('signrawtransactionwithwallet', [tx_funded, ])['hex']
tx_funded_decoded = ci.rpc('decoderawtransaction', [tx_funded, ])
tx_signed_decoded = ci.rpc('decoderawtransaction', [tx_signed, ])
assert tx_funded_decoded['txid'] == tx_signed_decoded['txid']
def test_007_hdwallet(self):
@@ -108,6 +115,132 @@ class TestLTC(BasicSwapTest):
assert (js_0['num_swapping'] == 0 and js_0['num_watched_outputs'] == 0)
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))
swap_clients = self.swap_clients
ci0 = swap_clients[0].ci(self.test_coin_from)
ci1 = swap_clients[1].ci(self.test_coin_from)
mweb_addr_0 = ci0.rpc_wallet('getnewaddress', ['mweb addr test 0', 'mweb'])
mweb_addr_1 = ci1.rpc_wallet('getnewaddress', ['mweb addr test 1', 'mweb'])
addr_info0 = ci0.rpc_wallet('getaddressinfo', [mweb_addr_0,])
assert (addr_info0['ismweb'] is True)
addr_info1 = ci1.rpc_wallet('getaddressinfo', [mweb_addr_1,])
assert (addr_info1['ismweb'] is True)
trusted_before = ci0.rpc_wallet('getbalances')['mine']['trusted']
ci0.rpc_wallet('sendtoaddress', [mweb_addr_0, 10.0])
assert (trusted_before - float(ci0.rpc_wallet('getbalances')['mine']['trusted']) < 0.1)
try:
pause_event.clear() # Stop mining
ci0.rpc_wallet('sendtoaddress', [mweb_addr_1, 10.0])
found_unconfirmed: bool = False
for i in range(20):
test_delay_event.wait(1)
ltc_wallet = read_json_api(TEST_HTTP_PORT + 1, 'wallets/ltc')
if float(ltc_wallet['unconfirmed']) == 10.0:
found_unconfirmed = True
break
finally:
pause_event.set()
assert (found_unconfirmed)
self.mineBlock()
txns = ci0.rpc_wallet('listtransactions')
utxos = ci0.rpc_wallet('listunspent')
balances = ci0.rpc_wallet('getbalances')
wi = ci0.rpc_wallet('getwalletinfo')
txid = ci0.rpc_wallet('sendtoaddress', [mweb_addr_1, 10.0])
self.mineBlock()
txns = ci1.rpc_wallet('listtransactions')
utxos = ci1.rpc_wallet('listunspent')
balances = ci1.rpc_wallet('getbalances')
wi = ci1.rpc_wallet('getwalletinfo')
mweb_tx = None
for utxo in utxos:
if utxo.get('address', '') == mweb_addr_1:
mweb_tx = utxo
assert (mweb_tx is not None)
tx = ci1.rpc_wallet('gettransaction', [mweb_tx['txid'],])
blockhash = tx['blockhash']
block = ci1.rpc('getblock', [blockhash, 3])
block = ci1.rpc('getblock', [blockhash, 0])
# TODO
def test_22_mweb_balance(self):
logging.info('---------- Test MWEB balance {}'.format(self.test_coin_from.name))
swap_clients = self.swap_clients
ci_mweb = swap_clients[0].ci(Coins.LTC_MWEB)
mweb_addr_0 = ci_mweb.getNewAddress()
addr_info0 = ci_mweb.rpc_wallet('getaddressinfo', [mweb_addr_0,])
assert (addr_info0['ismweb'] is True)
ltc_addr = read_json_api(TEST_HTTP_PORT + 0, 'wallets/ltc/nextdepositaddr')
ltc_mweb_addr = read_json_api(TEST_HTTP_PORT + 0, 'wallets/ltc_mweb/nextdepositaddr')
ltc_mweb_addr2 = read_json_api(TEST_HTTP_PORT + 0, 'wallets/ltc/newmwebaddress')
assert (ci_mweb.rpc_wallet('getaddressinfo', [ltc_addr,])['ismweb'] is False)
assert (ci_mweb.rpc_wallet('getaddressinfo', [ltc_mweb_addr,])['ismweb'] is True)
assert (ci_mweb.rpc_wallet('getaddressinfo', [ltc_mweb_addr2,])['ismweb'] is True)
post_json = {
'value': 10,
'address': ltc_mweb_addr,
'subfee': False,
}
json_rv = read_json_api(TEST_HTTP_PORT + 0, 'wallets/ltc/withdraw', post_json)
assert (len(json_rv['txid']) == 64)
self.mineBlock()
json_rv = read_json_api(TEST_HTTP_PORT + 0, 'wallets/ltc', post_json)
assert (json_rv['mweb_balance'] == 10.0)
mweb_address = json_rv['mweb_address']
post_json = {
'value': 11,
'address': mweb_address,
'subfee': False,
}
json_rv = read_json_api(TEST_HTTP_PORT + 0, 'wallets/ltc/withdraw', post_json)
assert (len(json_rv['txid']) == 64)
self.mineBlock()
json_rv = read_json_api(TEST_HTTP_PORT + 0, 'wallets/ltc_mweb', post_json)
assert (json_rv['mweb_balance'] == 21.0)
assert (json_rv['mweb_address'] == mweb_address)
ltc_address = json_rv['deposit_address']
# Check that spending the mweb balance takes from the correct wallet
post_json = {
'value': 1,
'address': ltc_address,
'subfee': False,
'type_from': 'mweb',
}
json_rv = read_json_api(TEST_HTTP_PORT + 0, 'wallets/ltc/withdraw', post_json)
assert (len(json_rv['txid']) == 64)
json_rv = read_json_api(TEST_HTTP_PORT + 0, 'wallets/ltc', post_json)
assert (json_rv['mweb_balance'] <= 20.0)
if __name__ == '__main__':
unittest.main()

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright (c) 2019-2023 tecnovert
# Copyright (c) 2019-2024 tecnovert
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -22,7 +22,8 @@ from coincurve.keys import (
PrivateKey)
from basicswap.util import i2b, h2b
from basicswap.util.crypto import ripemd160
from basicswap.util.crypto import ripemd160, hash160
from basicswap.util.network import is_private_ip_address
from basicswap.util.rfc2440 import rfc2440_hash_password
from basicswap.interface.btc import BTCInterface
from basicswap.interface.xmr import XMRInterface
@@ -36,6 +37,12 @@ from basicswap.util import (
DeserialiseNum,
validate_amount)
from basicswap.messages_pb2 import (
BidMessage,
BidMessage_v1Deprecated,
)
from basicswap.contrib.test_framework.script import hash160 as hash160_btc
class Test(unittest.TestCase):
REQUIRED_SETTINGS = {'blocks_confirmed': 1, 'conf_target': 1, 'use_segwit': True, 'connection_type': 'rpc'}
@@ -312,6 +319,34 @@ class Test(unittest.TestCase):
input_data = b'hash this'
assert (ripemd160(input_data).hex() == 'd5443a154f167e2c1332f6de72cfb4c6ab9c8c17')
def test_hash160(self):
# hash160 is RIPEMD(SHA256(data))
input_data = b'hash this'
assert (hash160(input_data).hex() == '072985b3583a4a71f548494a5e1d5f6b00d0fe13')
assert (hash160_btc(input_data).hex() == '072985b3583a4a71f548494a5e1d5f6b00d0fe13')
def test_protobuf(self):
# Ensure old protobuf templates can be read
msg_buf = BidMessage_v1Deprecated()
msg_buf.protocol_version = 2
serialised_msg = msg_buf.SerializeToString()
msg_buf_v2 = BidMessage()
msg_buf_v2.ParseFromString(serialised_msg)
assert (msg_buf_v2.protocol_version == 2)
def test_is_private_ip_address(self):
assert (is_private_ip_address('localhost'))
assert (is_private_ip_address('127.0.0.1'))
assert (is_private_ip_address('10.0.0.0'))
assert (is_private_ip_address('172.16.0.0'))
assert (is_private_ip_address('192.168.0.0'))
assert (is_private_ip_address('20.87.245.0') is False)
assert (is_private_ip_address('particl.io') is False)
if __name__ == '__main__':
unittest.main()

View File

@@ -101,7 +101,7 @@ class Test(BaseTest):
nonlocal ci
i = 0
while not delay_event.is_set():
unspents = ci.rpc_callback('listunspentblind')
unspents = ci.rpc_wallet('listunspentblind')
if len(unspents) >= 1:
return
delay_event.wait(delay_time)
@@ -113,8 +113,8 @@ class Test(BaseTest):
amount: int = ci.make_int(random.uniform(0.1, 2.0), r=1)
# Record unspents before createSCLockTx as the used ones will be locked
unspents = ci.rpc_callback('listunspentblind')
locked_utxos_before = ci.rpc_callback('listlockunspent')
unspents = ci.rpc_wallet('listunspentblind')
locked_utxos_before = ci.rpc_wallet('listlockunspent')
# fee_rate is in sats/kvB
fee_rate: int = 1000
@@ -131,33 +131,33 @@ class Test(BaseTest):
lock_tx = ci.fundSCLockTx(lock_tx, fee_rate, vkbv)
lock_tx = ci.signTxWithWallet(lock_tx)
unspents_after = ci.rpc_callback('listunspentblind')
locked_utxos_after = ci.rpc_callback('listlockunspent')
unspents_after = ci.rpc_wallet('listunspentblind')
locked_utxos_after = ci.rpc_wallet('listlockunspent')
assert (len(unspents) > len(unspents_after))
assert (len(locked_utxos_after) > len(locked_utxos_before))
lock_tx_decoded = ci.rpc_callback('decoderawtransaction', [lock_tx.hex()])
lock_tx_decoded = ci.rpc_wallet('decoderawtransaction', [lock_tx.hex()])
txid = lock_tx_decoded['txid']
vsize = lock_tx_decoded['vsize']
expect_fee_int = round(fee_rate * vsize / 1000)
expect_fee = ci.format_amount(expect_fee_int)
ci.rpc_callback('sendrawtransaction', [lock_tx.hex()])
rv = ci.rpc_callback('gettransaction', [txid])
ci.rpc_wallet('sendrawtransaction', [lock_tx.hex()])
rv = ci.rpc_wallet('gettransaction', [txid])
wallet_tx_fee = -ci.make_int(rv['details'][0]['fee'])
assert (wallet_tx_fee >= expect_fee_int)
assert (wallet_tx_fee - expect_fee_int < 20)
addr_out = ci.getNewAddress(True)
addrinfo = ci.rpc_callback('getaddressinfo', [addr_out,])
addrinfo = ci.rpc_wallet('getaddressinfo', [addr_out,])
pk_out = bytes.fromhex(addrinfo['pubkey'])
fee_info = {}
lock_spend_tx = ci.createSCLockSpendTx(lock_tx, lock_tx_script, pk_out, fee_rate, vkbv, fee_info=fee_info)
vsize_estimated: int = fee_info['vsize']
spend_tx_decoded = ci.rpc_callback('decoderawtransaction', [lock_spend_tx.hex()])
spend_tx_decoded = ci.rpc('decoderawtransaction', [lock_spend_tx.hex()])
txid = spend_tx_decoded['txid']
nonce = ci.getScriptLockTxNonce(vkbv)
@@ -172,12 +172,12 @@ class Test(BaseTest):
lock_tx_script,
]
lock_spend_tx = ci.setTxSignature(lock_spend_tx, witness_stack)
tx_decoded = ci.rpc_callback('decoderawtransaction', [lock_spend_tx.hex()])
tx_decoded = ci.rpc('decoderawtransaction', [lock_spend_tx.hex()])
vsize_actual: int = tx_decoded['vsize']
# Note: The fee is set allowing 9 bytes for the encoded fee amount, causing a small overestimate
assert (vsize_actual <= vsize_estimated and vsize_estimated - vsize_actual < 10)
assert (ci.rpc_callback('sendrawtransaction', [lock_spend_tx.hex()]) == txid)
assert (ci.rpc('sendrawtransaction', [lock_spend_tx.hex()]) == txid)
# Test chain b (no-script) lock tx size
v = ci.getNewSecretKey()
@@ -198,7 +198,7 @@ class Test(BaseTest):
lock_tx_b_spend = ci.getTransaction(lock_tx_b_spend_txid)
if lock_tx_b_spend is None:
lock_tx_b_spend = ci.getWalletTransaction(lock_tx_b_spend_txid)
lock_tx_b_spend_decoded = ci.rpc_callback('decoderawtransaction', [lock_tx_b_spend.hex()])
lock_tx_b_spend_decoded = ci.rpc('decoderawtransaction', [lock_tx_b_spend.hex()])
expect_vsize: int = ci.xmr_swap_b_lock_spend_tx_vsize()
assert (expect_vsize >= lock_tx_b_spend_decoded['vsize'])
@@ -472,7 +472,7 @@ class Test(BaseTest):
# Verify expected inputs were used
bid, _, _, _, _ = swap_clients[2].getXmrBidAndOffer(bid_id)
assert (bid.xmr_a_lock_tx)
wtx = ci.rpc_callback('gettransaction', [bid.xmr_a_lock_tx.txid.hex(),])
wtx = ci.rpc_wallet('gettransaction', [bid.xmr_a_lock_tx.txid.hex(),])
itx_after = ci.describeTx(wtx['hex'])
assert (len(itx_after['vin']) == len(itx_decoded['vin']))
for i, txin in enumerate(itx_decoded['vin']):

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright (c) 2019-2023 tecnovert
# Copyright (c) 2019-2024 tecnovert
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -52,6 +52,17 @@ from tests.basicswap.common import (
compare_bid_states,
extract_states_from_xu_file,
)
from basicswap.contrib.test_framework.messages import (
ToHex,
CTxIn,
COutPoint,
CTransaction,
CTxInWitness,
)
from basicswap.contrib.test_framework.script import (
CScript,
OP_CHECKLOCKTIMEVERIFY,
)
from .test_xmr import BaseTest, test_delay_event, callnoderpc
@@ -69,10 +80,10 @@ class Test(BaseTest):
super(Test, cls).setUpClass()
btc_addr1 = callnoderpc(1, 'getnewaddress', ['initial funds', 'bech32'], base_rpc_port=BTC_BASE_RPC_PORT)
ltc_addr1 = callnoderpc(1, 'getnewaddress', ['initial funds', 'bech32'], base_rpc_port=LTC_BASE_RPC_PORT)
ltc_addr1 = callnoderpc(1, 'getnewaddress', ['initial funds', 'bech32'], base_rpc_port=LTC_BASE_RPC_PORT, wallet='wallet.dat')
callnoderpc(0, 'sendtoaddress', [btc_addr1, 1000], base_rpc_port=BTC_BASE_RPC_PORT)
callnoderpc(0, 'sendtoaddress', [ltc_addr1, 1000], base_rpc_port=LTC_BASE_RPC_PORT)
callnoderpc(0, 'sendtoaddress', [ltc_addr1, 1000], base_rpc_port=LTC_BASE_RPC_PORT, wallet='wallet.dat')
wait_for_balance(test_delay_event, 'http://127.0.0.1:1801/json/wallets/btc', 'balance', 1000.0)
wait_for_balance(test_delay_event, 'http://127.0.0.1:1801/json/wallets/ltc', 'balance', 1000.0)
@@ -101,7 +112,6 @@ class Test(BaseTest):
def test_001_js_coins(self):
js_coins = read_json_api(1800, 'coins')
for c in Coins:
coin = next((x for x in js_coins if x['id'] == int(c)), None)
if c in (Coins.PART, Coins.BTC, Coins.LTC, Coins.PART_ANON, Coins.PART_BLIND):
@@ -114,14 +124,12 @@ class Test(BaseTest):
def test_002_lookup_rates(self):
rv = self.swap_clients[0].lookupRates(Coins.BTC, Coins.PART)
assert ('coingecko' in rv)
assert ('bittrex' in rv)
rv = self.swap_clients[0].lookupRates(Coins.LTC, Coins.BTC)
assert ('coingecko' in rv)
assert ('bittrex' in rv)
rv = read_json_api(1800, 'rateslist?from=PART&to=BTC')
assert len(rv) == 2
assert len(rv) == 1
def test_003_api(self):
logging.info('---------- Test API')
@@ -171,6 +179,12 @@ class Test(BaseTest):
rv = read_json_api(1800, 'automationstrategies/1')
assert (rv['label'] == 'Accept All')
sx_addr = read_json_api(1800, 'wallets/part/newstealthaddress')
assert (callnoderpc(0, 'getaddressinfo', [sx_addr, ])['isstealthaddress'] is True)
rv = read_json_api(1800, 'wallets/part')
assert ('locked_utxos' in rv)
def test_004_validateSwapType(self):
logging.info('---------- Test validateSwapType')
@@ -180,15 +194,17 @@ class Test(BaseTest):
(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.SELLER_FIRST),
(Coins.BTC, Coins.FIRO, SwapTypes.SELLER_FIRST),
(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.FIRO, Coins.BTC, SwapTypes.XMR_SWAP),
(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:
@@ -196,6 +212,77 @@ class Test(BaseTest):
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))
ci = self.swap_clients[0].ci(test_coin_from)
deploymentinfo = callnoderpc(0, 'getdeploymentinfo')
bip65_active = deploymentinfo['deployments']['bip65']['active']
assert (bip65_active)
chain_height = callnoderpc(0, 'getblockcount')
script = CScript([chain_height + 3, OP_CHECKLOCKTIMEVERIFY, ])
script_dest = ci.getScriptDest(script)
tx = CTransaction()
tx.nVersion = ci.txVersion()
tx.vout.append(ci.txoType()(ci.make_int(1.1), script_dest))
tx_hex = ToHex(tx)
tx_funded = callnoderpc(0, 'fundrawtransaction', [tx_hex])
utxo_pos = 0 if tx_funded['changepos'] == 1 else 1
tx_signed = callnoderpc(0, 'signrawtransactionwithwallet', [tx_funded['hex'], ])['hex']
txid = callnoderpc(0, 'sendrawtransaction', [tx_signed, ])
addr_out = callnoderpc(0, 'getnewaddress', ['cltv test', ])
pkh = ci.decodeAddress(addr_out)
script_out = ci.getScriptForPubkeyHash(pkh)
tx_spend = CTransaction()
tx_spend.nVersion = ci.txVersion()
tx_spend.nLockTime = chain_height + 3
tx_spend.vin.append(CTxIn(COutPoint(int(txid, 16), utxo_pos)))
tx_spend.vout.append(ci.txoType()(ci.make_int(1.0999), script_out))
tx_spend.wit.vtxinwit.append(CTxInWitness())
tx_spend.wit.vtxinwit[0].scriptWitness.stack = [script, ]
tx_spend_hex = ToHex(tx_spend)
tx_spend.nLockTime = chain_height + 2
tx_spend_invalid_hex = ToHex(tx_spend)
for tx_hex in [tx_spend_invalid_hex, tx_spend_hex]:
try:
txid = callnoderpc(0, 'sendrawtransaction', [tx_hex, ])
except Exception as e:
assert ('non-final' in str(e))
else:
assert False, 'Should fail'
self.waitForParticlHeight(chain_height + 3)
try:
txid = callnoderpc(0, 'sendrawtransaction', [tx_spend_invalid_hex, ])
except Exception as e:
assert ('Locktime requirement not satisfied' in str(e))
else:
assert False, 'Should fail'
chain_height = callnoderpc(0, 'getblockcount')
txid = callnoderpc(0, 'sendrawtransaction', [tx_spend_hex, ])
ro = callnoderpc(0, 'listreceivedbyaddress', [0, ])
sum_addr = 0
for entry in ro:
if entry['address'] == addr_out:
sum_addr += entry['amount']
assert (sum_addr == 1.0999)
# Ensure tx was mined
for i in range(5):
self.waitForParticlHeight(chain_height + i)
tx_wallet = callnoderpc(0, 'gettransaction', [txid, ])
if 'blockhash' in tx_wallet:
break
assert (len(tx_wallet['blockhash']) == 64)
def test_01_verifyrawtransaction(self):
txn = '0200000001eb6e5c4ebba4efa32f40c7314cad456a64008e91ee30b2dd0235ab9bb67fbdbb01000000ee47304402200956933242dde94f6cf8f195a470f8d02aef21ec5c9b66c5d3871594bdb74c9d02201d7e1b440de8f4da672d689f9e37e98815fb63dbc1706353290887eb6e8f7235012103dc1b24feb32841bc2f4375da91fa97834e5983668c2a39a6b7eadb60e7033f9d205a803b28fe2f86c17db91fa99d7ed2598f79b5677ffe869de2e478c0d1c02cc7514c606382012088a8201fe90717abb84b481c2a59112414ae56ec8acc72273642ca26cc7a5812fdc8f68876a914225fbfa4cb725b75e511810ac4d6f74069bdded26703520140b27576a914207eb66b2fd6ed9924d6217efc7fa7b38dfabe666888acffffffff01e0167118020000001976a9140044e188928710cecba8311f1cf412135b98145c88ac00000000'
prevout = {
@@ -486,7 +573,7 @@ class Test(BaseTest):
def test_12_withdrawal(self):
logging.info('---------- Test LTC withdrawals')
ltc_addr = callnoderpc(0, 'getnewaddress', ['Withdrawal test', 'legacy'], base_rpc_port=LTC_BASE_RPC_PORT)
ltc_addr = callnoderpc(0, 'getnewaddress', ['Withdrawal test', 'legacy'], base_rpc_port=LTC_BASE_RPC_PORT, wallet='wallet.dat')
wallets0 = read_json_api(TEST_HTTP_PORT + 0, 'wallets')
assert (float(wallets0['LTC']['balance']) > 100)
@@ -576,7 +663,7 @@ class Test(BaseTest):
js_w2 = read_json_api(1802, 'wallets')
if float(js_w2['PART']['balance']) < 100.0:
post_json = {
'value': 100,
'value': 100.0,
'address': js_w2['PART']['deposit_address'],
'subfee': False,
}
@@ -594,8 +681,8 @@ class Test(BaseTest):
'subfee': True,
}
json_rv = read_json_api(TEST_HTTP_PORT + 2, 'wallets/part/withdraw', post_json)
wait_for_balance(test_delay_event, 'http://127.0.0.1:1802/json/wallets/part', 'balance', 10.0)
assert (len(json_rv['txid']) == 64)
wait_for_balance(test_delay_event, 'http://127.0.0.1:1802/json/wallets/part', 'balance', 10.0)
# Create prefunded ITX
ci = swap_clients[2].ci(Coins.PART)
@@ -628,7 +715,7 @@ class Test(BaseTest):
# Verify expected inputs were used
bid, offer = swap_clients[2].getBidAndOffer(bid_id)
assert (bid.initiate_tx)
wtx = ci.rpc_callback('gettransaction', [bid.initiate_tx.txid.hex(),])
wtx = ci.rpc('gettransaction', [bid.initiate_tx.txid.hex(),])
itx_after = ci.describeTx(wtx['hex'])
assert (len(itx_after['vin']) == len(itx_decoded['vin']))
for i, txin in enumerate(itx_decoded['vin']):

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright (c) 2020-2023 tecnovert
# Copyright (c) 2020-2024 tecnovert
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -107,6 +107,7 @@ XMR_BASE_RPC_PORT = 21792
XMR_BASE_ZMQ_PORT = 22792
XMR_BASE_WALLET_RPC_PORT = 23792
signal_event = threading.Event() # Set if test was cancelled
test_delay_event = threading.Event()
RESET_TEST = make_boolean(os.getenv('RESET_TEST', 'true'))
@@ -204,9 +205,9 @@ def prepare_swapclient_dir(datadir, node_id, network_key, network_pubkey, with_c
'check_events_seconds': 1,
'check_xmr_swaps_seconds': 1,
'min_delay_event': 1,
'max_delay_event': 5,
'max_delay_event': 4,
'min_delay_event_short': 1,
'max_delay_event_short': 5,
'max_delay_event_short': 3,
'min_delay_retry': 2,
'max_delay_retry': 10,
'debug_ui': True,
@@ -255,6 +256,7 @@ def ltcCli(cmd, node_id=0):
def signal_handler(sig, frame):
logging.info('signal {} detected.'.format(sig))
signal_event.set()
test_delay_event.set()
@@ -336,6 +338,9 @@ class BaseTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
if signal_event.is_set():
raise ValueError('Test has been cancelled.')
test_delay_event.clear()
random.seed(time.time())
logger.propagate = False
@@ -530,29 +535,29 @@ class BaseTest(unittest.TestCase):
if cls.start_ltc_nodes:
num_blocks = 400
cls.ltc_addr = callnoderpc(0, 'getnewaddress', ['mining_addr', 'bech32'], base_rpc_port=LTC_BASE_RPC_PORT)
cls.ltc_addr = callnoderpc(0, 'getnewaddress', ['mining_addr', 'bech32'], base_rpc_port=LTC_BASE_RPC_PORT, wallet='wallet.dat')
logging.info('Mining %d Litecoin blocks to %s', num_blocks, cls.ltc_addr)
callnoderpc(0, 'generatetoaddress', [num_blocks, cls.ltc_addr], base_rpc_port=LTC_BASE_RPC_PORT)
callnoderpc(0, 'generatetoaddress', [num_blocks, cls.ltc_addr], base_rpc_port=LTC_BASE_RPC_PORT, wallet='wallet.dat')
num_blocks = 31
cls.ltc_addr = cls.swap_clients[0].ci(Coins.LTC).pubkey_to_address(void_block_rewards_pubkey)
logging.info('Mining %d Litecoin blocks to %s', num_blocks, cls.ltc_addr)
callnoderpc(0, 'generatetoaddress', [num_blocks, cls.ltc_addr], base_rpc_port=LTC_BASE_RPC_PORT)
callnoderpc(0, 'generatetoaddress', [num_blocks, cls.ltc_addr], base_rpc_port=LTC_BASE_RPC_PORT, wallet='wallet.dat')
# https://github.com/litecoin-project/litecoin/issues/807
# Block 432 is when MWEB activates. It requires a peg-in. You'll need to generate an mweb address and send some coins to it. Then it will allow you to mine the next block.
mweb_addr = callnoderpc(2, 'getnewaddress', ['mweb_addr', 'mweb'], base_rpc_port=LTC_BASE_RPC_PORT)
callnoderpc(0, 'sendtoaddress', [mweb_addr, 1], base_rpc_port=LTC_BASE_RPC_PORT)
mweb_addr = callnoderpc(2, 'getnewaddress', ['mweb_addr', 'mweb'], base_rpc_port=LTC_BASE_RPC_PORT, wallet='wallet.dat')
callnoderpc(0, 'sendtoaddress', [mweb_addr, 1], base_rpc_port=LTC_BASE_RPC_PORT, wallet='wallet.dat')
ltc_addr1 = callnoderpc(1, 'getnewaddress', ['initial addr'], base_rpc_port=LTC_BASE_RPC_PORT)
ltc_addr1 = callnoderpc(1, 'getnewaddress', ['initial addr'], base_rpc_port=LTC_BASE_RPC_PORT, wallet='wallet.dat')
for i in range(5):
callnoderpc(0, 'sendtoaddress', [ltc_addr1, 100], base_rpc_port=LTC_BASE_RPC_PORT)
callnoderpc(0, 'sendtoaddress', [ltc_addr1, 100], base_rpc_port=LTC_BASE_RPC_PORT, wallet='wallet.dat')
num_blocks = 69
cls.ltc_addr = cls.swap_clients[0].ci(Coins.LTC).pubkey_to_address(void_block_rewards_pubkey)
callnoderpc(0, 'generatetoaddress', [num_blocks, cls.ltc_addr], base_rpc_port=LTC_BASE_RPC_PORT)
callnoderpc(0, 'generatetoaddress', [num_blocks, cls.ltc_addr], base_rpc_port=LTC_BASE_RPC_PORT, wallet='wallet.dat')
checkForks(callnoderpc(0, 'getblockchaininfo', base_rpc_port=LTC_BASE_RPC_PORT))
checkForks(callnoderpc(0, 'getblockchaininfo', base_rpc_port=LTC_BASE_RPC_PORT, wallet='wallet.dat'))
num_blocks = 100
if cls.start_xmr_nodes:
@@ -618,6 +623,13 @@ class BaseTest(unittest.TestCase):
stopDaemons(cls.btc_daemons)
stopDaemons(cls.ltc_daemons)
cls.http_threads.clear()
cls.swap_clients.clear()
cls.part_daemons.clear()
cls.btc_daemons.clear()
cls.ltc_daemons.clear()
cls.xmr_daemons.clear()
super(BaseTest, cls).tearDownClass()
@classmethod
@@ -682,7 +694,7 @@ class Test(BaseTest):
amount: int = ci.make_int(random.uniform(0.1, 2.0), r=1)
# Record unspents before createSCLockTx as the used ones will be locked
unspents = ci.rpc_callback('listunspent')
unspents = ci.rpc('listunspent')
# fee_rate is in sats/kvB
fee_rate: int = 1000
@@ -698,10 +710,10 @@ class Test(BaseTest):
lock_tx = ci.fundSCLockTx(lock_tx, fee_rate)
lock_tx = ci.signTxWithWallet(lock_tx)
unspents_after = ci.rpc_callback('listunspent')
unspents_after = ci.rpc('listunspent')
assert (len(unspents) > len(unspents_after))
tx_decoded = ci.rpc_callback('decoderawtransaction', [lock_tx.hex()])
tx_decoded = ci.rpc('decoderawtransaction', [lock_tx.hex()])
txid = tx_decoded['txid']
vsize = tx_decoded['vsize']
@@ -722,8 +734,8 @@ class Test(BaseTest):
break
fee_value = in_value - out_value
ci.rpc_callback('sendrawtransaction', [lock_tx.hex()])
rv = ci.rpc_callback('gettransaction', [txid])
ci.rpc('sendrawtransaction', [lock_tx.hex()])
rv = ci.rpc('gettransaction', [txid])
wallet_tx_fee = -ci.make_int(rv['fee'])
assert (wallet_tx_fee == fee_value)
@@ -735,7 +747,7 @@ class Test(BaseTest):
lock_spend_tx = ci.createSCLockSpendTx(lock_tx, lock_tx_script, pkh_out, fee_rate, fee_info=fee_info)
vsize_estimated: int = fee_info['vsize']
tx_decoded = ci.rpc_callback('decoderawtransaction', [lock_spend_tx.hex()])
tx_decoded = ci.rpc('decoderawtransaction', [lock_spend_tx.hex()])
txid = tx_decoded['txid']
witness_stack = [
@@ -745,11 +757,11 @@ class Test(BaseTest):
lock_tx_script,
]
lock_spend_tx = ci.setTxSignature(lock_spend_tx, witness_stack)
tx_decoded = ci.rpc_callback('decoderawtransaction', [lock_spend_tx.hex()])
tx_decoded = ci.rpc('decoderawtransaction', [lock_spend_tx.hex()])
vsize_actual: int = tx_decoded['vsize']
assert (vsize_actual <= vsize_estimated and vsize_estimated - vsize_actual < 4)
assert (ci.rpc_callback('sendrawtransaction', [lock_spend_tx.hex()]) == txid)
assert (ci.rpc('sendrawtransaction', [lock_spend_tx.hex()]) == txid)
expect_vsize: int = ci.xmr_swap_a_lock_spend_tx_vsize()
assert (expect_vsize >= vsize_actual)
@@ -766,7 +778,7 @@ class Test(BaseTest):
lock_tx_b_spend = ci.getTransaction(lock_tx_b_spend_txid)
if lock_tx_b_spend is None:
lock_tx_b_spend = ci.getWalletTransaction(lock_tx_b_spend_txid)
lock_tx_b_spend_decoded = ci.rpc_callback('decoderawtransaction', [lock_tx_b_spend.hex()])
lock_tx_b_spend_decoded = ci.rpc('decoderawtransaction', [lock_tx_b_spend.hex()])
expect_vsize: int = ci.xmr_swap_b_lock_spend_tx_vsize()
assert (expect_vsize >= lock_tx_b_spend_decoded['vsize'])
@@ -1354,7 +1366,7 @@ class Test(BaseTest):
lock_tx_b_spend = ci.getTransaction(lock_tx_b_spend_txid)
if lock_tx_b_spend is None:
lock_tx_b_spend = ci.getWalletTransaction(lock_tx_b_spend_txid)
lock_tx_b_spend_decoded = ci.rpc_callback('decoderawtransaction', [lock_tx_b_spend.hex()])
lock_tx_b_spend_decoded = ci.rpc('decoderawtransaction', [lock_tx_b_spend.hex()])
expect_vsize: int = ci.xmr_swap_b_lock_spend_tx_vsize()
assert (expect_vsize >= lock_tx_b_spend_decoded['vsize'])
@@ -1501,7 +1513,7 @@ class Test(BaseTest):
# Verify expected inputs were used
bid, _, _, _, _ = swap_clients[2].getXmrBidAndOffer(bid_id)
assert (bid.xmr_a_lock_tx)
wtx = ci.rpc_callback('gettransaction', [bid.xmr_a_lock_tx.txid.hex(),])
wtx = ci.rpc('gettransaction', [bid.xmr_a_lock_tx.txid.hex(),])
itx_after = ci.describeTx(wtx['hex'])
assert (len(itx_after['vin']) == len(itx_decoded['vin']))
for i, txin in enumerate(itx_decoded['vin']):