31 Commits
dev ... bch

Author SHA1 Message Date
tecnovert
bd4291ab45 Add event when BCH mercy tx published. 2024-11-14 18:34:46 +02:00
tecnovert
bae735c144 api: Set correct default swap type for BCH. 2024-11-14 15:41:16 +02:00
tecnovert
18079457e8 Bump version. 2024-11-14 15:30:09 +02:00
tecnovert
ef7791cb14 Add debugind to prevent spending coin A lock tx. 2024-11-14 15:12:24 +02:00
tecnovert
4030dc9858 ui: Fix error when txid is unknown. 2024-11-14 00:04:10 +02:00
tecnovert
811fa15f26 Fix BCH exchange name, pipe daemon output to file from prepare. 2024-11-14 00:03:59 +02:00
tecnovert
4693d96c52 docker: Update templates for BCH. 2024-11-13 00:17:24 +02:00
tecnovert
3a5e40187a Switch BCH from using wallet watchonly to watching scripts in BSX.
Using wallet watchonly to find the lock transactions only seems to work with rescanblockchain.
2024-11-11 16:45:09 +02:00
tecnovert
c561efaba0 Add BTC type swipe tx mercy outputs. 2024-11-08 14:18:06 +02:00
tecnovert
9d7841da46 Test BCH mercy tx in reverse swaps. 2024-11-07 23:25:38 +02:00
tecnovert
09434f20e6 Check BCH mercy tx value. 2024-11-07 10:10:21 +02:00
tecnovert
92c7cb7223 Detect BCH mercy txn. 2024-11-07 02:48:15 +02:00
mainnet-pat
1a085ec97b Add support for mercy transactions for refund-refund path 2024-11-05 18:41:07 +00:00
tecnovert
97fcf177a9 api: Optionally display events with states. 2024-11-04 16:13:25 +02:00
tecnovert
b43c159dc4 Add display_name to chainparams. 2024-11-02 14:21:56 +02:00
tecnovert
5ee28d0aa3 Set Bitcoincash .conf file and port from basicswap.json. 2024-11-02 11:29:57 +02:00
tecnovert
5df9b044ab Add BCH to adaptor_swap_only_coins. 2024-11-02 11:21:57 +02:00
tecnovert
51d9685af0 tests: Add BCH to test_xmr_persistent 2024-11-02 11:21:53 +02:00
tecnovert
8e00753f97 tests: Move BCH code to BCH test file. 2024-11-02 11:21:46 +02:00
tecnovert
c0b94b3d7b Change BCH pidfile name and cli binary. 2024-11-02 11:19:52 +02:00
tecnovert
8637811c05 tests: Merge BCH tests. 2024-10-30 23:31:22 +02:00
tecnovert
f1dcef4971 Allow BCH as coin_to in ADS swaps. 2024-10-30 23:26:55 +02:00
mainnet-pat
aa9b9c1507 Gui fixes to bitcoincash 2024-10-30 15:03:41 +00:00
mainnet-pat
32df813731 Lint 2024-10-30 15:03:41 +00:00
mainnet-pat
ec1671911b Final fix to refunds path, increase test coverage 2024-10-30 15:03:41 +00:00
mainnet-pat
57513aeb06 All spend paths implemented, increasing test coverage, still some issues with refund spends and swipes to resolve 2024-10-30 15:03:41 +00:00
mainnet-pat
52e6e2b586 Continue extending bch interface and test coverage
Support reverse swap happy path
2024-10-30 15:03:41 +00:00
mainnet-pat
68256fdcf7 Cleanup imports, change bch ops to CScriptOps 2024-10-30 15:03:40 +00:00
mainnet-pat
c009d555e7 Continue building out the BCH interface
Adapt the swap flow to BCH specifics - txids change after funding and when signing inputs
BCH happy path (lock-spend) done
2024-10-30 15:03:12 +00:00
mainnet-pat
58b42c0d9a WIP continue implementing the BCH swap interface for XMR swap and atomic swap protocols 2024-10-30 15:03:12 +00:00
mainnet-pat
a4b411f1fd Add bitcoincash support for prepare and run scripts, add bitcoincash to testing suite, groundwork for bch-xmr atomic swap protocol 2024-10-30 15:03:08 +00:00
48 changed files with 2809 additions and 230 deletions

View File

@@ -6,7 +6,7 @@ lint_task:
- pip install flake8 codespell
script:
- flake8 --version
- PYTHONWARNINGS="ignore" flake8 --ignore=E501,F841,W503 --exclude=basicswap/contrib,basicswap/interface/contrib,messages_pb2.py,.eggs,.tox,bin/install_certifi.py
- PYTHONWARNINGS="ignore" flake8 --ignore=E501,F841,W503,E702,E131 --exclude=basicswap/contrib,basicswap/interface/contrib,messages_pb2.py,.eggs,.tox,bin/install_certifi.py
- codespell --check-filenames --disable-colors --quiet-level=7 --ignore-words=tests/lint/spelling.ignore-words.txt -S .git,.eggs,.tox,pgp,*.pyc,*basicswap/contrib,*basicswap/interface/contrib,*mnemonics.py,bin/install_certifi.py,*basicswap/static
test_task:
@@ -16,6 +16,7 @@ test_task:
- BIN_DIR: /tmp/cached_bin
- PARTICL_BINDIR: ${BIN_DIR}/particl
- BITCOIN_BINDIR: ${BIN_DIR}/bitcoin
- BITCOINCASH_BINDIR: ${BIN_DIR}/bitcoincash
- LITECOIN_BINDIR: ${BIN_DIR}/litecoin
- XMR_BINDIR: ${BIN_DIR}/monero
setup_script:
@@ -29,7 +30,7 @@ test_task:
fingerprint_script:
- basicswap-prepare -v
populate_script:
- basicswap-prepare --bindir=/tmp/cached_bin --preparebinonly --withcoins=particl,bitcoin,litecoin,monero
- basicswap-prepare --bindir=/tmp/cached_bin --preparebinonly --withcoins=particl,bitcoin,bitcoincash,litecoin,monero
script:
- cd "${CIRRUS_WORKING_DIR}"
- export DATADIRS="${TEST_DIR}"

View File

@@ -20,7 +20,7 @@ jobs:
pip install flake8 codespell
- name: Running flake8
run: |
PYTHONWARNINGS="ignore" flake8 --ignore=E501,F841,W503 --exclude=basicswap/contrib,basicswap/interface/contrib,messages_pb2.py,.eggs,.tox,bin/install_certifi.py
flake8 --ignore=E501,F841,W503 --per-file-ignores="basicswap/interface/bch.py:E131,E702" --exclude=basicswap/contrib,basicswap/interface/contrib,messages_pb2.py,.eggs,.tox,bin/install_certifi.py
- name: Running codespell
run: |
codespell --check-filenames --disable-colors --quiet-level=7 --ignore-words=tests/lint/spelling.ignore-words.txt -S .git,.eggs,.tox,pgp,*.pyc,*basicswap/contrib,*basicswap/interface/contrib,*mnemonics.py,bin/install_certifi.py,*basicswap/static

3
.gitignore vendored
View File

@@ -13,3 +13,6 @@ __pycache__
# geckodriver.log
*.log
docker/.env
# vscode dev container settings
compose-dev.yaml

View File

@@ -1,3 +1,3 @@
name = "basicswap"
__version__ = "0.14.1"
__version__ = "0.14.2"

View File

@@ -126,7 +126,8 @@ class BaseApp:
def callcoincli(self, coin_type, params, wallet=None, timeout=None):
bindir = self.coin_clients[coin_type]['bindir']
datadir = self.coin_clients[coin_type]['datadir']
command_cli = os.path.join(bindir, chainparams[coin_type]['name'] + '-cli' + ('.exe' if os.name == 'nt' else ''))
cli_bin: str = chainparams[coin_type].get('cli_binname', chainparams[coin_type]['name'] + '-cli')
command_cli = os.path.join(bindir, cli_bin + ('.exe' if os.name == 'nt' else ''))
args = [command_cli, ]
if self.chain != 'mainnet':
args.append('-' + self.chain)

File diff suppressed because it is too large Load Diff

View File

@@ -64,6 +64,7 @@ class SwapTypes(IntEnum):
SELLER_FIRST_2MSG = auto()
BUYER_FIRST_2MSG = auto()
XMR_SWAP = auto()
XMR_BCH_SWAP = auto()
class OfferStates(IntEnum):
@@ -136,6 +137,8 @@ class TxTypes(IntEnum):
ITX_PRE_FUNDED = auto()
BCH_MERCY = auto()
class ActionTypes(IntEnum):
ACCEPT_BID = auto()
@@ -183,6 +186,8 @@ class EventLogTypes(IntEnum):
PTX_REDEEM_PUBLISHED = auto()
PTX_REFUND_PUBLISHED = auto()
LOCK_TX_B_IN_MEMPOOL = auto()
BCH_MERCY_TX_PUBLISHED = auto()
BCH_MERCY_TX_FOUND = auto()
class XmrSplitMsgTypes(IntEnum):
@@ -194,6 +199,7 @@ class DebugTypes(IntEnum):
NONE = 0
BID_STOP_AFTER_COIN_A_LOCK = auto()
BID_DONT_SPEND_COIN_A_LOCK_REFUND = auto()
BID_DONT_SPEND_COIN_A_LOCK_REFUND2 = auto() # continues
CREATE_INVALID_COIN_B_LOCK = auto()
BUYER_STOP_AFTER_ITX = auto()
MAKE_INVALID_PTX = auto()
@@ -205,6 +211,9 @@ class DebugTypes(IntEnum):
DONT_CONFIRM_PTX = auto()
OFFER_LOCK_2_VALUE_INC = auto()
BID_STOP_AFTER_COIN_B_LOCK = auto()
BID_DONT_SPEND_COIN_B_LOCK = auto()
WAIT_FOR_COIN_B_LOCK_BEFORE_REFUND = auto()
BID_DONT_SPEND_COIN_A_LOCK = auto()
class NotificationTypes(IntEnum):
@@ -356,7 +365,9 @@ def strTxType(tx_type):
if tx_type == TxTypes.XMR_SWAP_B_LOCK:
return 'Chain B Lock Tx'
if tx_type == TxTypes.ITX_PRE_FUNDED:
return 'Funded mock initiate tx'
return 'Funded mock initiate Tx'
if tx_type == TxTypes.BCH_MERCY:
return 'BCH Mercy Tx'
return 'Unknown'
@@ -444,6 +455,10 @@ def describeEventEntry(event_type, event_msg):
return 'Participate tx redeem tx published'
if event_type == EventLogTypes.PTX_REFUND_PUBLISHED:
return 'Participate tx refund tx published'
if event_type == EventLogTypes.BCH_MERCY_TX_FOUND:
return 'BCH mercy tx found'
if event_type == EventLogTypes.BCH_MERCY_TX_PUBLISHED:
return 'Lock tx B mercy tx published'
def getVoutByAddress(txjs, p2sh):

113
basicswap/bin/prepare.py Executable file → Normal file
View File

@@ -37,7 +37,7 @@ from basicswap.ui.util import getCoinName
from basicswap.util import toBool
from basicswap.util.network import urlretrieve, make_reporthook
from basicswap.util.rfc2440 import rfc2440_hash_password
from basicswap.bin.run import startDaemon, startXmrWalletDaemon
from basicswap.bin.run import startDaemon, startXmrWalletDaemon, getCoreBinName, getCoreBinArgs, getWalletBinName
PARTICL_VERSION = os.getenv('PARTICL_VERSION', '23.2.7.0')
PARTICL_VERSION_TAG = os.getenv('PARTICL_VERSION_TAG', '')
@@ -49,6 +49,9 @@ LITECOIN_VERSION_TAG = os.getenv('LITECOIN_VERSION_TAG', '')
BITCOIN_VERSION = os.getenv('BITCOIN_VERSION', '26.0')
BITCOIN_VERSION_TAG = os.getenv('BITCOIN_VERSION_TAG', '')
BITCOINCASH_VERSION = os.getenv('BITCOINCASH_VERSION', '27.1.0')
BITCOINCASH_VERSION_TAG = os.getenv('BITCOINCASH_VERSION_TAG', '')
MONERO_VERSION = os.getenv('MONERO_VERSION', '0.18.3.4')
MONERO_VERSION_TAG = os.getenv('MONERO_VERSION_TAG', '')
XMR_SITE_COMMIT = '3751c0d7987a9e78324a718c32c008e2ec91b339' # Lock hashes.txt to monero version
@@ -84,6 +87,7 @@ SKIP_GPG_VALIDATION = toBool(os.getenv('SKIP_GPG_VALIDATION', 'false'))
known_coins = {
'particl': (PARTICL_VERSION, PARTICL_VERSION_TAG, ('tecnovert',)),
'bitcoin': (BITCOIN_VERSION, BITCOIN_VERSION_TAG, ('laanwj',)),
'bitcoincash': (BITCOINCASH_VERSION, BITCOINCASH_VERSION_TAG, ('Calin_Culianu',)),
'litecoin': (LITECOIN_VERSION, LITECOIN_VERSION_TAG, ('davidburkett38',)),
'decred': (DCR_VERSION, DCR_VERSION_TAG, ('decred_release',)),
'namecoin': ('0.18.0', '', ('JeremyRand',)),
@@ -113,6 +117,7 @@ expected_key_ids = {
'reuben': ('1290A1D0FA7EE109',),
'nav_builder': ('2782262BF6E7FADB',),
'decred_release': ('6D897EDF518A031D',),
'Calin_Culianu': ('21810A542031C02C',),
}
USE_PLATFORM = os.getenv('USE_PLATFORM', platform.system())
@@ -135,7 +140,8 @@ BIN_ARCH = os.getenv('BIN_ARCH', BIN_ARCH)
FILE_EXT = os.getenv('FILE_EXT', FILE_EXT)
logger = logging.getLogger()
logger.level = logging.INFO
LOG_LEVEL = logging.DEBUG
logger.level = LOG_LEVEL
if not len(logger.handlers):
logger.addHandler(logging.StreamHandler(sys.stdout))
@@ -186,6 +192,13 @@ BTC_ONION_PORT = int(os.getenv('BTC_ONION_PORT', 8334))
BTC_RPC_USER = os.getenv('BTC_RPC_USER', '')
BTC_RPC_PWD = os.getenv('BTC_RPC_PWD', '')
BCH_RPC_HOST = os.getenv('BCH_RPC_HOST', '127.0.0.1')
BCH_RPC_PORT = int(os.getenv('BCH_RPC_PORT', 19997))
BCH_ONION_PORT = int(os.getenv('BCH_ONION_PORT', 8335))
BCH_PORT = int(os.getenv('BCH_PORT', 19798))
BCH_RPC_USER = os.getenv('BCH_RPC_USER', '')
BCH_RPC_PWD = os.getenv('BCH_RPC_PWD', '')
DCR_RPC_HOST = os.getenv('DCR_RPC_HOST', '127.0.0.1')
DCR_RPC_PORT = int(os.getenv('DCR_RPC_PORT', 9109))
DCR_WALLET_RPC_HOST = os.getenv('DCR_WALLET_RPC_HOST', '127.0.0.1')
@@ -308,6 +321,7 @@ def setConnectionParameters(timeout: int = 5, allow_set_tor: bool = True):
# Set low timeout for urlretrieve connections
socket.setdefaulttimeout(timeout)
logger.level = logging.INFO
def popConnectionParameters() -> None:
@@ -315,6 +329,7 @@ def popConnectionParameters() -> None:
socket.socket = default_socket
socket.getaddrinfo = default_socket_getaddrinfo
socket.setdefaulttimeout(default_socket_timeout)
logger.level = LOG_LEVEL
def getRemoteFileLength(url: str) -> (int, bool):
@@ -513,8 +528,14 @@ def extractCore(coin, version_data, settings, bin_dir, release_path, extra_opts=
return
dir_name = 'dashcore' if coin == 'dash' else coin
dir_name = 'bitcoin-cash-node' if coin == 'bitcoincash' else coin
if coin == 'decred':
bins = ['dcrd', 'dcrwallet']
elif coin == 'bitcoincash':
bins = ['bitcoind', 'bitcoin-cli', 'bitcoin-tx']
versions = version.split('.')
if int(versions[0]) >= 22 or int(versions[1]) >= 19:
bins.append('bitcoin-wallet')
else:
bins = [coin + 'd', coin + '-cli', coin + '-tx']
versions = version.split('.')
@@ -696,6 +717,11 @@ def prepareCore(coin, version_data, settings, data_dir, extra_opts={}):
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)
elif coin == 'bitcoincash':
release_filename = 'bitcoin-cash-node-{}-{}.{}'.format(version, BIN_ARCH, FILE_EXT)
release_url = 'https://github.com/bitcoin-cash-node/bitcoin-cash-node/releases/download/v{}/{}'.format(version, release_filename)
assert_filename = 'SHA256SUMS.{}.asc.Calin_Culianu'.format(version)
assert_url = 'https://gitlab.com/bitcoin-cash-node/announcements/-/raw/master/release-sigs/%s/%s' % (version, assert_filename)
elif coin == 'namecoin':
release_url = 'https://beta.namecoin.org/files/namecoin-core/namecoin-core-{}/{}'.format(version, release_filename)
assert_filename = '{}-{}-{}-build.assert'.format(coin, os_name, version.rsplit('.', 1)[0])
@@ -738,7 +764,7 @@ def prepareCore(coin, version_data, settings, data_dir, extra_opts={}):
if not os.path.exists(assert_path):
downloadFile(assert_url, assert_path)
if coin not in ('firo', ):
if coin not in ('firo', 'bitcoincash',):
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)
@@ -798,11 +824,13 @@ def prepareCore(coin, version_data, settings, data_dir, extra_opts={}):
pubkeyurls.append('https://git.wownero.com/wownero/wownero/raw/branch/master/utils/gpg_keys/wowario.asc')
if coin == 'firo':
pubkeyurls.append('https://firo.org/reuben.asc')
if coin == 'bitcoincash':
pubkeyurls.append('https://gitlab.com/bitcoin-cash-node/bitcoin-cash-node/-/raw/master/contrib/gitian-signing/pubkeys.txt')
if ADD_PUBKEY_URL != '':
pubkeyurls.append(ADD_PUBKEY_URL + '/' + pubkey_filename)
if coin in ('monero', 'wownero', 'firo'):
if coin in ('monero', 'wownero', 'firo', 'bitcoincash',):
with open(assert_path, 'rb') as fp:
verified = gpg.verify_file(fp)
@@ -837,7 +865,6 @@ def prepareCore(coin, version_data, settings, data_dir, extra_opts={}):
verified = gpg.verify_file(fp, assert_path)
ensureValidSignatureBy(verified, signing_key_name)
extractCore(coin, version_data, settings, bin_dir, release_path, extra_opts)
@@ -870,7 +897,8 @@ def prepareDataDir(coin, settings, chain, particl_mnemonic, extra_opts={}):
os.makedirs(data_dir)
if coin in ('wownero', 'monero'):
core_conf_path = os.path.join(data_dir, coin + 'd.conf')
conf_filename: str = core_settings.get('config_filename', coin + 'd.conf')
core_conf_path = os.path.join(data_dir, conf_filename)
if os.path.exists(core_conf_path):
exitWithError('{} exists'.format(core_conf_path))
with open(core_conf_path, 'w') as fp:
@@ -907,14 +935,12 @@ def prepareDataDir(coin, settings, chain, particl_mnemonic, extra_opts={}):
for opt_line in wownerod_proxy_config:
fp.write(opt_line + '\n')
if coin in ('wownero', 'monero'):
wallets_dir = core_settings.get('walletsdir', data_dir)
if not os.path.exists(wallets_dir):
os.makedirs(wallets_dir)
wallet_conf_path = os.path.join(wallets_dir, coin + '-wallet-rpc.conf')
if coin == 'monero':
wallet_conf_path = os.path.join(wallets_dir, 'monero_wallet.conf')
wallet_conf_filename: str = core_settings.get('wallet_config_filename', 'monero_wallet.conf' if coin == 'monero' else (coin + '-wallet-rpc.conf'))
wallet_conf_path = os.path.join(wallets_dir, wallet_conf_filename)
if os.path.exists(wallet_conf_path):
exitWithError('{} exists'.format(wallet_conf_path))
with open(wallet_conf_path, 'w') as fp:
@@ -944,7 +970,8 @@ def prepareDataDir(coin, settings, chain, particl_mnemonic, extra_opts={}):
if coin == 'decred':
chainname = 'simnet' if chain == 'regtest' else chain
core_conf_path = os.path.join(data_dir, 'dcrd.conf')
conf_filename: str = core_settings.get('config_filename', 'dcrd.conf')
core_conf_path = os.path.join(data_dir, conf_filename)
if os.path.exists(core_conf_path):
exitWithError('{} exists'.format(core_conf_path))
with open(core_conf_path, 'w') as fp:
@@ -961,7 +988,8 @@ def prepareDataDir(coin, settings, chain, particl_mnemonic, extra_opts={}):
if tor_control_password is not None:
writeTorSettings(fp, coin, core_settings, tor_control_password)
wallet_conf_path = os.path.join(data_dir, 'dcrwallet.conf')
wallet_conf_filename: str = core_settings.get('wallet_config_filename', 'dcrwallet.conf')
wallet_conf_path = os.path.join(data_dir, wallet_conf_filename)
if os.path.exists(wallet_conf_path):
exitWithError('{} exists'.format(wallet_conf_path))
with open(wallet_conf_path, 'w') as fp:
@@ -979,7 +1007,8 @@ def prepareDataDir(coin, settings, chain, particl_mnemonic, extra_opts={}):
return
core_conf_path = os.path.join(data_dir, coin + '.conf')
core_conf_name: str = core_settings.get('config_filename', coin + '.conf')
core_conf_path = os.path.join(data_dir, core_conf_name)
if os.path.exists(core_conf_path):
exitWithError('{} exists'.format(core_conf_path))
with open(core_conf_path, 'w') as fp:
@@ -1032,6 +1061,11 @@ def prepareDataDir(coin, settings, chain, particl_mnemonic, extra_opts={}):
fp.write('fallbackfee=0.0002\n')
if BTC_RPC_USER != '':
fp.write('rpcauth={}:{}${}\n'.format(BTC_RPC_USER, salt, password_to_hmac(salt, BTC_RPC_PWD)))
elif coin == 'bitcoincash':
fp.write('prune=4000\n')
fp.write('pid=bitcoincashd.pid\n')
if BCH_RPC_USER != '':
fp.write('rpcauth={}:{}${}\n'.format(BCH_RPC_USER, salt, password_to_hmac(salt, BCH_RPC_PWD)))
elif coin == 'namecoin':
fp.write('prune=2000\n')
elif coin == 'pivx':
@@ -1108,14 +1142,14 @@ def modify_tor_config(settings, coin, tor_control_password=None, enable=False, e
data_dir = coin_settings['datadir']
if coin in ('monero', 'wownero'):
core_conf_path = os.path.join(data_dir, coin + 'd.conf')
core_conf_name: str = coin_settings.get('config_filename', coin + 'd.conf')
core_conf_path = os.path.join(data_dir, core_conf_name)
if not os.path.exists(core_conf_path):
exitWithError('{} does not exist'.format(core_conf_path))
wallets_dir = coin_settings.get('walletsdir', data_dir)
wallet_conf_path = os.path.join(wallets_dir, coin + '-wallet-rpc.conf')
if coin == 'monero':
wallet_conf_path = os.path.join(wallets_dir, 'monero_wallet.conf')
wallet_conf_filename: str = coin_settings.get('wallet_config_filename', 'monero_wallet.conf' if coin == 'monero' else (coin + '-wallet-rpc.conf'))
wallet_conf_path = os.path.join(wallets_dir, wallet_conf_filename)
if not os.path.exists(wallet_conf_path):
exitWithError('{} does not exist'.format(wallet_conf_path))
@@ -1170,10 +1204,8 @@ def modify_tor_config(settings, coin, tor_control_password=None, enable=False, e
coin_settings['trusted_daemon'] = extra_opts.get('trust_remote_node', 'auto')
return
if coin == 'decred':
config_path = os.path.join(data_dir, 'dcrd.conf')
else:
config_path = os.path.join(data_dir, coin + '.conf')
core_conf_name: str = coin_settings.get('config_filename', 'dcrd.conf' if coin == 'decred' else (coin + '.conf'))
config_path = os.path.join(data_dir, core_conf_name)
if not os.path.exists(config_path):
exitWithError('{} does not exist'.format(config_path))
@@ -1182,6 +1214,8 @@ def modify_tor_config(settings, coin, tor_control_password=None, enable=False, e
default_onionport = 0
if coin == 'bitcoin':
default_onionport = BTC_ONION_PORT
if coin == 'bitcoincash':
default_onionport = BCH_ONION_PORT
elif coin == 'particl':
default_onionport = PART_ONION_PORT
elif coin == 'litecoin':
@@ -1292,9 +1326,11 @@ def test_particl_encryption(data_dir, settings, chain, use_tor_proxy):
c = Coins.PART
coin_name = 'particl'
coin_settings = settings['chainclients'][coin_name]
daemon_args += getCoreBinArgs(c, coin_settings)
extra_config = {'stdout_to_file': True}
if coin_settings['manage_daemon']:
filename = coin_name + 'd' + ('.exe' if os.name == 'nt' else '')
daemons.append(startDaemon(coin_settings['datadir'], coin_settings['bindir'], filename, daemon_args))
filename: str = getCoreBinName(c, coin_settings, coin_name + 'd')
daemons.append(startDaemon(coin_settings['datadir'], coin_settings['bindir'], filename, daemon_args, extra_config=extra_config))
swap_client.setDaemonPID(c, daemons[-1].handle.pid)
swap_client.setCoinRunParams(c)
swap_client.createCoinInterface(c)
@@ -1344,22 +1380,25 @@ def initialise_wallets(particl_wallet_mnemonic, with_coins, data_dir, settings,
if c == Coins.XMR:
if coin_settings['manage_wallet_daemon']:
filename = coin_name + '-wallet-rpc' + ('.exe' if os.name == 'nt' else '')
filename: str = getWalletBinName(c, coin_settings, coin_name + '-wallet-rpc')
daemons.append(startXmrWalletDaemon(coin_settings['datadir'], coin_settings['bindir'], filename))
elif c == Coins.WOW:
if coin_settings['manage_wallet_daemon']:
filename = coin_name + '-wallet-rpc' + ('.exe' if os.name == 'nt' else '')
filename: str = getWalletBinName(c, coin_settings, coin_name + '-wallet-rpc')
daemons.append(startXmrWalletDaemon(coin_settings['datadir'], coin_settings['bindir'], filename))
elif c == Coins.DCR:
pass
else:
if coin_settings['manage_daemon']:
filename = coin_name + 'd' + ('.exe' if os.name == 'nt' else '')
filename: str = getCoreBinName(c, coin_settings, coin_name + 'd')
coin_args = ['-nofindpeers', '-nostaking'] if c == Coins.PART else []
coin_args += getCoreBinArgs(c, coin_settings)
if c == Coins.FIRO:
coin_args += ['-hdseed={}'.format(swap_client.getWalletKey(Coins.FIRO, 1).hex())]
daemons.append(startDaemon(coin_settings['datadir'], coin_settings['bindir'], filename, daemon_args + coin_args))
extra_config = {'stdout_to_file': True}
daemons.append(startDaemon(coin_settings['datadir'], coin_settings['bindir'], filename, daemon_args + coin_args, extra_config=extra_config))
swap_client.setDaemonPID(c, daemons[-1].handle.pid)
swap_client.setCoinRunParams(c)
swap_client.createCoinInterface(c)
@@ -1375,7 +1414,7 @@ def initialise_wallets(particl_wallet_mnemonic, with_coins, data_dir, settings,
'--pass={}'.format(dcr_password),
]
filename = 'dcrwallet' + ('.exe' if os.name == 'nt' else '')
filename: str = getWalletBinName(c, coin_settings, 'dcrwallet')
args = [os.path.join(coin_settings['bindir'], filename), '--create'] + extra_opts
hex_seed = swap_client.getWalletKey(Coins.DCR, 1).hex()
createDCRWallet(args, hex_seed, logger, threading.Event())
@@ -1755,6 +1794,21 @@ def main():
'conf_target': 2,
'core_version_group': 22,
},
'bitcoincash': {
'connection_type': 'rpc',
'manage_daemon': shouldManageDaemon('BCH'),
'rpchost': BCH_RPC_HOST,
'rpcport': BCH_RPC_PORT + port_offset,
'onionport': BCH_ONION_PORT + port_offset,
'datadir': os.getenv('BCH_DATA_DIR', os.path.join(data_dir, 'bitcoincash')),
'bindir': os.path.join(bin_dir, 'bitcoincash'),
'port': BCH_PORT + port_offset,
'config_filename': 'bitcoin.conf',
'use_segwit': False,
'blocks_confirmed': 1,
'conf_target': 2,
'core_version_group': 22,
},
'litecoin': {
'connection_type': 'rpc',
'manage_daemon': shouldManageDaemon('LTC'),
@@ -1787,6 +1841,7 @@ def main():
'blocks_confirmed': 2,
'conf_target': 2,
'core_type_group': 'dcr',
'config_filename': 'dcrd.conf',
'min_relay_fee': 0.00001,
},
'namecoin': {
@@ -1823,6 +1878,7 @@ def main():
'rpctimeout': 60,
'walletrpctimeout': 120,
'walletrpctimeoutlong': 600,
'wallet_config_filename': 'monero_wallet.conf',
'core_type_group': 'xmr',
},
'pivx': {
@@ -1917,6 +1973,9 @@ def main():
if BTC_RPC_USER != '':
chainclients['bitcoin']['rpcuser'] = BTC_RPC_USER
chainclients['bitcoin']['rpcpassword'] = BTC_RPC_PWD
if BCH_RPC_USER != '':
chainclients['bitcoincash']['rpcuser'] = BCH_RPC_USER
chainclients['bitcoincash']['rpcpassword'] = BCH_RPC_PWD
if XMR_RPC_USER != '':
chainclients['monero']['rpcuser'] = XMR_RPC_USER
chainclients['monero']['rpcpassword'] = XMR_RPC_PWD

View File

@@ -55,10 +55,10 @@ def signal_handler(sig, frame):
def startDaemon(node_dir, bin_dir, daemon_bin, opts=[], extra_config={}):
daemon_bin = os.path.expanduser(os.path.join(bin_dir, daemon_bin))
datadir_path = os.path.expanduser(node_dir)
# Rewrite litecoin.conf for 0.21.3
# TODO: Remove
ltc_conf_path = os.path.join(datadir_path, 'litecoin.conf')
if os.path.exists(ltc_conf_path):
config_to_add = ['blockfilterindex=0', 'peerblockfilters=0']
@@ -69,7 +69,7 @@ def startDaemon(node_dir, bin_dir, daemon_bin, opts=[], extra_config={}):
config_to_add.remove(line)
if len(config_to_add) > 0:
logging.info('Rewriting litecoin.conf')
logger.info('Rewriting litecoin.conf')
shutil.copyfile(ltc_conf_path, ltc_conf_path + '.last')
with open(ltc_conf_path, 'a') as fp:
for line in config_to_add:
@@ -80,7 +80,8 @@ def startDaemon(node_dir, bin_dir, daemon_bin, opts=[], extra_config={}):
if add_datadir:
args.append('-datadir=' + datadir_path)
args += opts
logging.info('Starting node ' + daemon_bin + ' ' + (('-datadir=' + node_dir) if add_datadir else ''))
logger.info('Starting node {}'.format(daemon_bin))
logger.debug('Arguments {}'.format(' '.join(args)))
opened_files = []
if extra_config.get('stdout_to_file', False):
@@ -91,10 +92,11 @@ def startDaemon(node_dir, bin_dir, daemon_bin, opts=[], extra_config={}):
stdout_dest = subprocess.PIPE
stderr_dest = subprocess.PIPE
shell: bool = False
if extra_config.get('use_shell', False):
str_args = ' '.join(args)
return Daemon(subprocess.Popen(str_args, shell=True, stdin=subprocess.PIPE, stdout=stdout_dest, stderr=stderr_dest, cwd=datadir_path), opened_files)
return Daemon(subprocess.Popen(args, stdin=subprocess.PIPE, stdout=stdout_dest, stderr=stderr_dest, cwd=datadir_path), opened_files)
args = ' '.join(args)
shell = True
return Daemon(subprocess.Popen(args, shell=shell, stdin=subprocess.PIPE, stdout=stdout_dest, stderr=stderr_dest, cwd=datadir_path), opened_files)
def startXmrDaemon(node_dir, bin_dir, daemon_bin, opts=[]):
@@ -103,7 +105,8 @@ def startXmrDaemon(node_dir, bin_dir, daemon_bin, opts=[]):
datadir_path = os.path.expanduser(node_dir)
config_filename = 'wownerod.conf' if daemon_bin.startswith('wow') else 'monerod.conf'
args = [daemon_path, '--non-interactive', '--config-file=' + os.path.join(datadir_path, config_filename)] + opts
logging.info('Starting node {} --data-dir={}'.format(daemon_path, node_dir))
logger.info('Starting node {}'.format(daemon_bin))
logger.debug('Arguments {}'.format(' '.join(args)))
# return subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
file_stdout = open(os.path.join(datadir_path, 'core_stdout.log'), 'w')
@@ -112,8 +115,8 @@ def startXmrDaemon(node_dir, bin_dir, daemon_bin, opts=[]):
def startXmrWalletDaemon(node_dir, bin_dir, wallet_bin, opts=[]):
daemon_bin = os.path.expanduser(os.path.join(bin_dir, wallet_bin))
args = [daemon_bin, '--non-interactive']
daemon_path = os.path.expanduser(os.path.join(bin_dir, wallet_bin))
args = [daemon_path, '--non-interactive']
needs_rewrite: bool = False
config_to_remove = ['daemon-address=', 'untrusted-daemon=', 'trusted-daemon=', 'proxy=']
@@ -127,19 +130,20 @@ def startXmrWalletDaemon(node_dir, bin_dir, wallet_bin, opts=[]):
with open(config_path) as fp:
for line in fp:
if any(line.startswith(config_line) for config_line in config_to_remove):
logging.warning('Found old config in monero_wallet.conf: {}'.format(line.strip()))
logger.warning('Found old config in monero_wallet.conf: {}'.format(line.strip()))
needs_rewrite = True
args += opts
if needs_rewrite:
logging.info('Rewriting wallet config')
logger.info('Rewriting wallet config')
shutil.copyfile(config_path, config_path + '.last')
with open(config_path + '.last') as fp_from, open(config_path, 'w') as fp_to:
for line in fp_from:
if not any(line.startswith(config_line) for config_line in config_to_remove):
fp_to.write(line)
logging.info('Starting wallet daemon {} --wallet-dir={}'.format(daemon_bin, node_dir))
logger.info('Starting wallet daemon {}'.format(wallet_bin))
logger.debug('Arguments {}'.format(' '.join(args)))
# TODO: return subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=data_dir)
wallet_stdout = open(os.path.join(data_dir, 'wallet_stdout.log'), 'w')
@@ -166,8 +170,25 @@ def ws_message_received(client, server, message):
swap_client.log.debug(f'ws_message_received {client["id"]} {message}')
def getCoreBinName(coin_id: int, coin_settings, default_name: str) -> str:
return coin_settings.get('core_binname', chainparams[coin_id].get('core_binname', default_name)) + ('.exe' if os.name == 'nt' else '')
def getWalletBinName(coin_id: int, coin_settings, default_name: str) -> str:
return coin_settings.get('wallet_binname', chainparams[coin_id].get('wallet_binname', default_name)) + ('.exe' if os.name == 'nt' else '')
def getCoreBinArgs(coin_id: int, coin_settings):
extra_args = []
if 'config_filename' in coin_settings:
extra_args.append('--conf=' + coin_settings['config_filename'])
if 'port' in coin_settings:
extra_args.append('--port=' + str(int(coin_settings['port'])))
return extra_args
def runClient(fp, data_dir, chain, start_only_coins):
global swap_client
global swap_client, logger
daemons = []
pids = []
threads = []
@@ -188,6 +209,7 @@ def runClient(fp, data_dir, chain, start_only_coins):
settings = json.load(fs)
swap_client = BasicSwap(fp, data_dir, settings, chain)
logger = swap_client.log
if os.path.exists(pids_path):
with open(pids_path) as fd:
@@ -214,7 +236,8 @@ def runClient(fp, data_dir, chain, start_only_coins):
if c in ('monero', 'wownero'):
if v['manage_daemon'] is True:
swap_client.log.info(f'Starting {display_name} daemon')
filename = c + 'd' + ('.exe' if os.name == 'nt' else '')
filename: str = getCoreBinName(coin_id, v, c + 'd')
daemons.append(startXmrDaemon(v['datadir'], v['bindir'], filename))
pid = daemons[-1].handle.pid
swap_client.log.info('Started {} {}'.format(filename, pid))
@@ -240,7 +263,8 @@ def runClient(fp, data_dir, chain, start_only_coins):
opts.append(daemon_rpcuser + ':' + daemon_rpcpass)
opts.append('--trusted-daemon' if trusted_daemon else '--untrusted-daemon')
filename = c + '-wallet-rpc' + ('.exe' if os.name == 'nt' else '')
filename: str = getWalletBinName(coin_id, v, c + '-wallet-rpc')
daemons.append(startXmrWalletDaemon(v['datadir'], v['bindir'], filename, opts))
pid = daemons[-1].handle.pid
swap_client.log.info('Started {} {}'.format(filename, pid))
@@ -253,7 +277,7 @@ def runClient(fp, data_dir, chain, start_only_coins):
use_shell: bool = True if os.name == 'nt' else False
if v['manage_daemon'] is True:
swap_client.log.info(f'Starting {display_name} daemon')
filename = 'dcrd' + ('.exe' if os.name == 'nt' else '')
filename: str = getCoreBinName(coin_id, v, 'dcrd')
extra_config = {'add_datadir': False, 'stdout_to_file': True, 'stdout_filename': 'dcrd_stdout.log', 'use_shell': use_shell}
daemons.append(startDaemon(appdata, v['bindir'], filename, opts=extra_opts, extra_config=extra_config))
@@ -262,7 +286,7 @@ def runClient(fp, data_dir, chain, start_only_coins):
if v['manage_wallet_daemon'] is True:
swap_client.log.info(f'Starting {display_name} wallet daemon')
filename = 'dcrwallet' + ('.exe' if os.name == 'nt' else '')
filename: str = getWalletBinName(coin_id, v, 'dcrwallet')
wallet_pwd = v['wallet_pwd']
if wallet_pwd == '':
@@ -280,8 +304,9 @@ def runClient(fp, data_dir, chain, start_only_coins):
if v['manage_daemon'] is True:
swap_client.log.info(f'Starting {display_name} daemon')
filename = c + 'd' + ('.exe' if os.name == 'nt' else '')
daemons.append(startDaemon(v['datadir'], v['bindir'], filename))
filename: str = getCoreBinName(coin_id, v, c + 'd')
extra_opts = getCoreBinArgs(coin_id, v)
daemons.append(startDaemon(v['datadir'], v['bindir'], filename, opts=extra_opts))
pid = daemons[-1].handle.pid
pids.append((c, pid))
swap_client.setDaemonPID(c, pid)

View File

@@ -30,6 +30,7 @@ class Coins(IntEnum):
NAV = 14
LTC_MWEB = 15
# ZANO = 16
BCH = 17
chainparams = {
@@ -279,13 +280,13 @@ chainparams = {
Coins.PIVX: {
'name': 'pivx',
'ticker': 'PIVX',
'display_name': 'PIVX',
'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,
'mainnet': {
'rpcport': 51473,
'pubkey_address': 30,
@@ -432,7 +433,51 @@ chainparams = {
'min_amount': 1000,
'max_amount': 100000 * COIN,
}
}
},
Coins.BCH: {
'name': 'bitcoincash',
'ticker': 'BCH',
'display_name': 'Bitcoin Cash',
'message_magic': 'Bitcoin Signed Message:\n',
'blocks_target': 60 * 2,
'decimal_places': 8,
'has_cltv': True,
'has_csv': True,
'has_segwit': False,
'cli_binname': 'bitcoin-cli',
'core_binname': 'bitcoind',
'mainnet': {
'rpcport': 8332,
'pubkey_address': 0,
'script_address': 5,
'key_prefix': 128,
'hrp': 'bitcoincash',
'bip44': 0,
'min_amount': 1000,
'max_amount': 100000 * COIN,
},
'testnet': {
'rpcport': 18332,
'pubkey_address': 111,
'script_address': 196,
'key_prefix': 239,
'hrp': 'bchtest',
'bip44': 1,
'min_amount': 1000,
'max_amount': 100000 * COIN,
'name': 'testnet3',
},
'regtest': {
'rpcport': 18443,
'pubkey_address': 111,
'script_address': 196,
'key_prefix': 239,
'hrp': 'bchreg',
'bip44': 1,
'min_amount': 1000,
'max_amount': 100000 * COIN,
}
},
}
ticker_map = {}

View File

@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019-2023 tecnovert
# Copyright (c) 2019-2024 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -36,3 +36,5 @@ NAMECOIN_TX = os.getenv('NAMECOIN_TX', 'namecoin-tx' + bin_suffix)
XMR_BINDIR = os.path.expanduser(os.getenv('XMR_BINDIR', os.path.join(DEFAULT_TEST_BINDIR, 'monero')))
XMRD = os.getenv('XMRD', 'monerod' + bin_suffix)
XMR_WALLET_RPC = os.getenv('XMR_WALLET_RPC', 'monero-wallet-rpc' + bin_suffix)
# NOTE: Adding coin definitions here is deprecated. Please add in coin test file.

View File

@@ -52,6 +52,7 @@ class CoinInterface:
self.setDefaults()
self._network = network
self._mx_wallet = threading.Lock()
self._altruistic = True
def setDefaults(self):
self._unknown_wallet_seed = True
@@ -66,8 +67,8 @@ class CoinInterface:
def coin_name(self) -> str:
coin_chainparams = chainparams[self.coin_type()]
if coin_chainparams.get('use_ticker_as_name', False):
return coin_chainparams['ticker']
if 'display_name' in coin_chainparams:
return coin_chainparams['display_name']
return coin_chainparams['name'].capitalize()
def ticker(self) -> str:
@@ -166,6 +167,9 @@ class CoinInterface:
def checkWallets(self) -> int:
return 1
def altruistic(self) -> bool:
return self._altruistic
class AdaptorSigInterface():
def getScriptLockTxDummyWitness(self, script: bytes):

843
basicswap/interface/bch.py Normal file
View File

@@ -0,0 +1,843 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (c) 2024 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
from typing import Union
from basicswap.contrib.test_framework.messages import COutPoint, CTransaction, CTxIn
from basicswap.util import b2h, b2i, ensure, i2h
from basicswap.util.script import decodePushData, decodeScriptNum
from .btc import BTCInterface, ensure_op, findOutput
from basicswap.rpc import make_rpc_func
from basicswap.chainparams import Coins
from basicswap.interface.contrib.bch_test_framework.cashaddress import Address
from basicswap.util.crypto import hash160, sha256
from basicswap.interface.contrib.bch_test_framework.script import (
OP_TXINPUTCOUNT,
OP_1,
OP_NUMEQUALVERIFY,
OP_TXOUTPUTCOUNT,
OP_0,
OP_UTXOVALUE,
OP_OUTPUTVALUE,
OP_SUB,
OP_UTXOTOKENCATEGORY,
OP_OUTPUTTOKENCATEGORY,
OP_EQUALVERIFY,
OP_UTXOTOKENCOMMITMENT,
OP_OUTPUTTOKENCOMMITMENT,
OP_UTXOTOKENAMOUNT,
OP_OUTPUTTOKENAMOUNT,
OP_INPUTSEQUENCENUMBER,
OP_NOTIF,
OP_OUTPUTBYTECODE,
OP_OVER,
OP_CHECKDATASIG,
OP_ELSE,
OP_CHECKSEQUENCEVERIFY,
OP_DROP,
OP_EQUAL,
OP_ENDIF,
OP_HASH160,
OP_DUP,
OP_CHECKSIG,
OP_HASH256,
)
from basicswap.contrib.test_framework.script import OP_RETURN, CScript
from coincurve.keys import (
PrivateKey,
PublicKey,
)
from coincurve.ecdsaotves import (
ecdsaotves_enc_sign,
ecdsaotves_enc_verify,
ecdsaotves_dec_sig,
ecdsaotves_rec_enc_key,
)
class BCHInterface(BTCInterface):
@staticmethod
def coin_type():
return Coins.BCH
@staticmethod
def xmr_swap_a_lock_spend_tx_vsize() -> int:
return 302
@staticmethod
def watch_blocks_for_scripts() -> bool:
# TODO: BCH Watchonly: Remove when BCH watchonly works.
return True
def __init__(self, coin_settings, network, swap_client=None):
super(BCHInterface, self).__init__(coin_settings, network, swap_client)
# No multiwallet support
self.swap_client = swap_client
self.rpc_wallet = make_rpc_func(self._rpcport, self._rpcauth, host=self._rpc_host)
def has_segwit(self) -> bool:
# bch does not have segwit, but we return true here to avoid extra checks in basicswap.py
return True
def getExchangeName(self, exchange_name: str) -> str:
return 'bitcoin-cash'
def getNewAddress(self, use_segwit: bool = False, label: str = 'swap_receive') -> str:
args = [label]
return self.rpc_wallet('getnewaddress', args)
def getUnspentsByAddr(self):
unspent_addr = dict()
unspent = self.rpc_wallet('listunspent')
for u in unspent:
if u.get('spendable', False) is False:
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
# returns pkh
def decodeAddress(self, address: str) -> bytes:
return bytes(Address.from_string(address).payload)
def encodeSegwitAddress(self, script):
raise ValueError('Segwit not supported')
def decodeSegwitAddress(self, addr):
raise ValueError('Segwit not supported')
def getSCLockScriptAddress(self, lock_script: bytes) -> str:
lock_tx_dest = self.getScriptDest(lock_script)
address = self.encodeScriptDest(lock_tx_dest)
if not self.isAddressMine(address, or_watch_only=True):
# Expects P2WSH nested in BIP16_P2SH
ro = self.rpc('importaddress', [lock_tx_dest.hex(), 'bid lock', False, True])
addr_info = self.rpc('validateaddress', [address])
return address
def importWatchOnlyAddress(self, address: str, label: str):
self.rpc_wallet('importaddress', [address, label, False, True])
def createRawFundedTransaction(self, addr_to: str, amount: int, sub_fee: bool = False, lock_unspents: bool = True) -> str:
txn = self.rpc('createrawtransaction', [[], {addr_to: self.format_amount(amount)}])
options = {
'lockUnspents': lock_unspents,
# 'conf_target': self._conf_target,
}
if sub_fee:
options['subtractFeeFromOutputs'] = [0,]
return self.rpc_wallet('fundrawtransaction', [txn, options])['hex']
def getScriptForPubkeyHash(self, pkh: bytes) -> bytearray:
# Return P2PKH
return CScript([OP_DUP, OP_HASH160, pkh, OP_EQUALVERIFY, OP_CHECKSIG])
def encodeScriptDest(self, script_dest: bytes) -> str:
# Extract hash from script
script_hash = script_dest[2:-1]
return self.sh_to_address(script_hash)
def sh_to_address(self, sh: bytes) -> str:
assert (len(sh) == 20 or len(sh) == 32)
network = self._network.upper()
address = None
if len(sh) == 20:
address = Address("P2SH20" if network == "MAINNET" else "P2SH20-" + network, sh)
else:
address = Address("P2SH32" if network == "MAINNET" else "P2SH32-" + network, sh)
return address.cash_address()
def getDestForScriptHash(self, script_hash):
assert (len(script_hash) == 20 or len(script_hash) == 32)
if len(script_hash) == 20:
return CScript([OP_HASH160, script_hash, OP_EQUAL])
else:
return CScript([OP_HASH256, script_hash, OP_EQUAL])
def withdrawCoin(self, value: float, addr_to: str, subfee: bool):
params = [addr_to, value, '', '', subfee, 0, False]
return self.rpc_wallet('sendtoaddress', params)
def getBLockSpendTxFee(self, tx, fee_rate: int) -> int:
add_bytes = 107
size = len(tx.serialize_with_witness()) + add_bytes
pay_fee = round(fee_rate * size / 1000)
self._log.info(f'BLockSpendTx fee_rate, size, fee: {fee_rate}, {size}, {pay_fee}.')
return pay_fee
def findTxnByHash(self, txid_hex: str):
# Only works for wallet txns
try:
rv = self.rpc('gettransaction', [txid_hex])
except Exception as ex:
self._log.debug('findTxnByHash getrawtransaction failed: {}'.format(txid_hex))
return None
if 'confirmations' in rv and rv['confirmations'] >= self.blocks_confirmed:
block_height = self.getBlockHeader(rv['blockhash'])['height']
return {'txid': txid_hex, 'amount': 0, 'height': block_height}
return None
def getLockTxHeight(self, txid: bytes, dest_address: str, bid_amount: int, rescan_from: int, find_index: bool = False, vout: int = -1):
'''
TODO: BCH Watchonly
Replace with importWatchOnlyAddress when it works again
Currently importing the watchonly address only works if rescanblockchain is run on every iteration
'''
if txid is None:
self._log.debug('TODO: getLockTxHeight')
return None
found_vout = None
# Search for txo at vout 0 and 1 if vout is not known
if vout is None:
test_range = range(2)
else:
test_range = (vout, )
for try_vout in test_range:
try:
txout = self.rpc('gettxout', [txid.hex(), try_vout, True])
addresses = txout['scriptPubKey']['addresses']
if len(addresses) != 1 or addresses[0] != dest_address:
continue
if self.make_int(txout['value']) != bid_amount:
self._log.warning('getLockTxHeight found txout {} with incorrect amount {}'.format(txid.hex(), txout['value']))
continue
found_vout = try_vout
break
except Exception as e:
# self._log.warning('gettxout {}'.format(e))
return None
if found_vout is None:
return None
block_height: int = 0
confirmations: int = 0 if 'confirmations' not in txout else txout['confirmations']
# TODO: Better way?
if confirmations > 0:
block_height = self.getChainHeight() - confirmations
rv = {
'txid': txid.hex(),
'depth': confirmations,
'index': found_vout,
'height': block_height}
return rv
def genScriptLockTxScript(self, ci, Kal: bytes, Kaf: bytes, **kwargs) -> CScript:
mining_fee: int = kwargs['mining_fee'] if 'mining_fee' in kwargs else 1000
out_1: bytes = kwargs['out_1']
out_2: bytes = kwargs['out_2']
public_key: bytes = kwargs['public_key'] if 'public_key' in kwargs else Kal
timelock: int = kwargs['timelock']
# fmt: off
return CScript([
# // v4.1.0-CashTokens-Optimized
# // Based on swaplock.cash v4.1.0-CashTokens
#
# // Alice has XMR, wants BCH and/or CashTokens.
# // Bob has BCH and/or CashTokens, wants XMR.
#
# // Verify 1-in-1-out TX form
OP_TXINPUTCOUNT,
OP_1, OP_NUMEQUALVERIFY,
OP_TXOUTPUTCOUNT,
OP_1, OP_NUMEQUALVERIFY,
# // int miningFee
mining_fee,
# // Verify pre-agreed mining fee and that the rest of BCH is forwarded
# // to the output.
OP_0, OP_UTXOVALUE,
OP_0, OP_OUTPUTVALUE,
OP_SUB, OP_NUMEQUALVERIFY,
# # // Verify that any CashTokens are forwarded to the output.
OP_0, OP_UTXOTOKENCATEGORY,
OP_0, OP_OUTPUTTOKENCATEGORY,
OP_EQUALVERIFY,
OP_0, OP_UTXOTOKENCOMMITMENT,
OP_0, OP_OUTPUTTOKENCOMMITMENT,
OP_EQUALVERIFY,
OP_0, OP_UTXOTOKENAMOUNT,
OP_0, OP_OUTPUTTOKENAMOUNT,
OP_NUMEQUALVERIFY,
# // If sequence is not used then it is a regular swap TX.
OP_0, OP_INPUTSEQUENCENUMBER,
OP_NOTIF,
# // bytes aliceOutput
out_1,
# // Verify that the BCH and/or CashTokens are forwarded to Alice's
# // output.
OP_0, OP_OUTPUTBYTECODE,
OP_OVER, OP_EQUALVERIFY,
# // pubkey bobPubkeyVES
public_key,
# // Require Alice to decrypt and publish Bob's VES signature.
# // The "message" signed is simply a sha256 hash of Alice's output
# // locking bytecode.
# // By decrypting Bob's VES and publishing it, Alice reveals her
# // XMR key share to Bob.
OP_CHECKDATASIG,
# // If a TX using this path is mined then Alice gets her BCH.
# // Bob uses the revealed XMR key share to collect his XMR.
# // Refund will become available when timelock expires, and it would
# // expire because Alice didn't collect on time, either of her own accord
# // or because Bob bailed out and withheld the encrypted signature.
OP_ELSE,
# // int timelock_0
timelock,
# // Verify refund timelock.
OP_CHECKSEQUENCEVERIFY, OP_DROP,
# // bytes refundLockingBytecode
out_2,
# // Verify that the BCH and/or CashTokens are forwarded to Refund
# // contract.
OP_0, OP_OUTPUTBYTECODE,
OP_EQUAL,
# // BCH and/or CashTokens are simply forwarded to Refund contract.
OP_ENDIF
])
# fmt: on
def pubkey_to_segwit_address(self, pk: bytes) -> str:
raise NotImplementedError()
def pkh_to_address(self, pkh: bytes) -> str:
# pkh is ripemd160(sha256(pk))
assert (len(pkh) == 20)
network = self._network.upper()
address = Address("P2PKH" if network == "MAINNET" else "P2PKH-" + network, pkh)
return address.cash_address()
def encodeSharedAddress(self, Kbv: bytes, Kbs: bytes) -> str:
return self.pkh_to_address(hash160(Kbs))
def addressToLockingBytecode(self, address: str) -> bytes:
return b'\x76\xa9\x14' + bytes(Address.from_string(address).payload) + b'\x88\xac'
def getSpendableBalance(self) -> int:
return self.make_int(self.rpc_wallet('getbalance', ["*", 1, False]))
def getScriptDest(self, script):
return self.scriptToP2SH32LockingBytecode(script)
def scriptToP2SH32LockingBytecode(self, script: Union[bytes, str]) -> bytes:
return CScript([
OP_HASH256,
sha256(sha256(script)),
OP_EQUAL,
])
def createSCLockTx(self, value: int, script: bytearray, vkbv: bytes = None) -> bytes:
tx = CTransaction()
tx.nVersion = self.txVersion()
tx.vout.append(self.txoType()(value, self.getScriptDest(script)))
return tx.serialize_without_witness()
def getTxSize(self, tx: CTransaction) -> int:
return len(tx.serialize_without_witness())
def getScriptScriptSig(self, script: bytes, ves: bytes = None) -> bytes:
if ves is not None:
return CScript([ves, script])
else:
return CScript([script])
def createSCLockSpendTx(self, tx_lock_bytes, script_lock, pkh_dest, tx_fee_rate, vkbv=None, fee_info={}, **kwargs):
# tx_fee_rate in this context is equal to `mining_fee` contract param
ves = kwargs['ves'] if 'ves' in kwargs else None
tx_lock = self.loadTx(tx_lock_bytes)
output_script = self.getScriptDest(script_lock)
locked_n = findOutput(tx_lock, output_script)
ensure(locked_n is not None, 'Output not found in tx')
locked_coin = tx_lock.vout[locked_n].nValue
tx_lock.rehash()
tx_lock_id_int = tx_lock.sha256
tx = CTransaction()
tx.nVersion = self.txVersion()
tx.vin.append(CTxIn(COutPoint(tx_lock_id_int, locked_n),
scriptSig=self.getScriptScriptSig(script_lock, ves),
nSequence=0))
tx.vout.append(self.txoType()(locked_coin, self.getScriptForPubkeyHash(pkh_dest)))
pay_fee = tx_fee_rate
tx.vout[0].nValue = locked_coin - pay_fee
size = self.getTxSize(tx)
fee_info['fee_paid'] = pay_fee
fee_info['rate_used'] = tx_fee_rate
fee_info['size'] = size
# vsize is the same as size for BCH
fee_info['vsize'] = size
tx.rehash()
self._log.info('createSCLockSpendTx %s:\n fee_rate, size, fee: %ld, %ld, %ld.',
i2h(tx.sha256), tx_fee_rate, size, pay_fee)
return tx.serialize_without_witness()
def createSCLockRefundTx(self, tx_lock_bytes, script_lock, Kal, Kaf, lock1_value, csv_val, tx_fee_rate, vkbv=None, **kwargs):
tx_lock = CTransaction()
tx_lock = self.loadTx(tx_lock_bytes)
output_script = self.getScriptDest(script_lock)
locked_n = findOutput(tx_lock, output_script)
ensure(locked_n is not None, 'Output not found in tx')
locked_coin = tx_lock.vout[locked_n].nValue
tx_lock.rehash()
tx_lock_id_int = tx_lock.sha256
refund_script = kwargs['refund_lock_tx_script']
tx = CTransaction()
tx.nVersion = self.txVersion()
tx.vin.append(CTxIn(COutPoint(tx_lock_id_int, locked_n),
nSequence=kwargs['timelock'] if 'timelock' in kwargs else lock1_value,
scriptSig=self.getScriptScriptSig(script_lock, None)))
tx.vout.append(self.txoType()(locked_coin, self.getScriptDest(refund_script)))
pay_fee = kwargs['mining_fee'] if 'mining_fee' in kwargs else tx_fee_rate
tx.vout[0].nValue = locked_coin - pay_fee
size = self.getTxSize(tx)
vsize = size
tx.rehash()
self._log.info('createSCLockRefundTx %s:\n fee_rate, vsize, fee: %ld, %ld, %ld.',
i2h(tx.sha256), tx_fee_rate, vsize, pay_fee)
return tx.serialize_without_witness(), refund_script, tx.vout[0].nValue
def createSCLockRefundSpendTx(self, tx_lock_refund_bytes, script_lock_refund, pkh_refund_to, tx_fee_rate, vkbv=None, **kwargs):
# it is not possible to create the refund spend tx without the prior knowledge of the VES which is part of transaction preimage
# but it is better and more secure to create a lock spend transaction committing to zero VES than returning static data
kwargs['ves'] = bytes(73)
return self.createSCLockSpendTx(tx_lock_refund_bytes, script_lock_refund, pkh_refund_to, tx_fee_rate, vkbv, **kwargs)
def createSCLockRefundSpendToFTx(self, tx_lock_refund_bytes, script_lock_refund, pkh_dest, tx_fee_rate, vkbv=None, kbsf=None):
# lock refund swipe tx
# Sends the coinA locked coin to the follower
tx_lock_refund = self.loadTx(tx_lock_refund_bytes)
output_script = self.getScriptDest(script_lock_refund)
locked_n = findOutput(tx_lock_refund, output_script)
ensure(locked_n is not None, 'Output not found in tx')
locked_coin = tx_lock_refund.vout[locked_n].nValue
mining_fee, out_1, out_2, public_key, timelock = self.extractScriptLockScriptValues(script_lock_refund)
tx_lock_refund.rehash()
tx_lock_refund_hash_int = tx_lock_refund.sha256
tx = CTransaction()
tx.nVersion = self.txVersion()
tx.vin.append(CTxIn(COutPoint(tx_lock_refund_hash_int, locked_n),
nSequence=timelock,
scriptSig=self.getScriptScriptSig(script_lock_refund, None)))
tx.vout.append(self.txoType()(locked_coin, CScript(out_2)))
size = self.getTxSize(tx)
vsize = size
pay_fee = mining_fee
tx.vout[0].nValue = locked_coin - pay_fee
tx.rehash()
self._log.info('createSCLockRefundSpendToFTx %s:\n fee_rate, vsize, fee: %ld, %ld, %ld.',
i2h(tx.sha256), tx_fee_rate, vsize, pay_fee)
return tx.serialize_without_witness()
def signTx(self, key_bytes: bytes, tx_bytes: bytes, input_n: int, prevout_script: bytes, prevout_value: int) -> bytes:
# simply sign the entire tx data, as this is not a preimage signature
eck = PrivateKey(key_bytes)
return eck.sign(sha256(tx_bytes), hasher=None)
def verifyTxSig(self, tx_bytes: bytes, sig: bytes, K: bytes, input_n: int, prevout_script: bytes, prevout_value: int) -> bool:
# simple ecdsa signature verification
return self.verifyDataSig(tx_bytes, sig, K)
def verifyDataSig(self, data: bytes, sig: bytes, K: bytes) -> bool:
# simple ecdsa signature verification
pubkey = PublicKey(K)
return pubkey.verify(sig, sha256(data), hasher=None)
def setTxSignature(self, tx_bytes: bytes, stack) -> bytes:
return tx_bytes
def extractScriptLockScriptValuesFromScriptSig(self, script_bytes):
signature, nb = decodePushData(script_bytes, 0)
if nb == len(script_bytes):
unlock_script = signature[:]
signature = None
else:
unlock_script, _ = decodePushData(script_bytes, nb)
mining_fee, out_1, out_2, public_key, timelock = self.extractScriptLockScriptValues(unlock_script)
return signature, mining_fee, out_1, out_2, public_key, timelock
def extractScriptLockScriptValues(self, script_bytes):
# see BCHInterface.genScriptLockTxScript for reference
o = 0
script_len = len(script_bytes)
# TODO: stricter script_len checks
ensure_op(script_bytes[o] == OP_TXINPUTCOUNT); o += 1
ensure_op(script_bytes[o] == OP_1); o += 1
ensure_op(script_bytes[o] == OP_NUMEQUALVERIFY); o += 1
ensure_op(script_bytes[o] == OP_TXOUTPUTCOUNT); o += 1
ensure_op(script_bytes[o] == OP_1); o += 1
ensure_op(script_bytes[o] == OP_NUMEQUALVERIFY); o += 1
mining_fee, nb = decodeScriptNum(script_bytes, o); o += nb
ensure_op(script_bytes[o] == OP_0); o += 1
ensure_op(script_bytes[o] == OP_UTXOVALUE); o += 1
ensure_op(script_bytes[o] == OP_0); o += 1
ensure_op(script_bytes[o] == OP_OUTPUTVALUE); o += 1
ensure_op(script_bytes[o] == OP_SUB); o += 1
ensure_op(script_bytes[o] == OP_NUMEQUALVERIFY); o += 1
ensure_op(script_bytes[o] == OP_0); o += 1
ensure_op(script_bytes[o] == OP_UTXOTOKENCATEGORY); o += 1
ensure_op(script_bytes[o] == OP_0); o += 1
ensure_op(script_bytes[o] == OP_OUTPUTTOKENCATEGORY); o += 1
ensure_op(script_bytes[o] == OP_EQUALVERIFY); o += 1
ensure_op(script_bytes[o] == OP_0); o += 1
ensure_op(script_bytes[o] == OP_UTXOTOKENCOMMITMENT); o += 1
ensure_op(script_bytes[o] == OP_0); o += 1
ensure_op(script_bytes[o] == OP_OUTPUTTOKENCOMMITMENT); o += 1
ensure_op(script_bytes[o] == OP_EQUALVERIFY); o += 1
ensure_op(script_bytes[o] == OP_0); o += 1
ensure_op(script_bytes[o] == OP_UTXOTOKENAMOUNT); o += 1
ensure_op(script_bytes[o] == OP_0); o += 1
ensure_op(script_bytes[o] == OP_OUTPUTTOKENAMOUNT); o += 1
ensure_op(script_bytes[o] == OP_NUMEQUALVERIFY); o += 1
ensure_op(script_bytes[o] == OP_0); o += 1
ensure_op(script_bytes[o] == OP_INPUTSEQUENCENUMBER); o += 1
ensure_op(script_bytes[o] == OP_NOTIF); o += 1
out_1, nb = decodePushData(script_bytes, o); o += nb
ensure_op(script_bytes[o] == OP_0); o += 1
ensure_op(script_bytes[o] == OP_OUTPUTBYTECODE); o += 1
ensure_op(script_bytes[o] == OP_OVER); o += 1
ensure_op(script_bytes[o] == OP_EQUALVERIFY); o += 1
public_key, nb = decodePushData(script_bytes, o); o += nb
ensure_op(script_bytes[o] == OP_CHECKDATASIG); o += 1
ensure_op(script_bytes[o] == OP_ELSE); o += 1
timelock, nb = decodeScriptNum(script_bytes, o); o += nb
ensure_op(script_bytes[o] == OP_CHECKSEQUENCEVERIFY); o += 1
ensure_op(script_bytes[o] == OP_DROP); o += 1
out_2, nb = decodePushData(script_bytes, o); o += nb
ensure_op(script_bytes[o] == OP_0); o += 1
ensure_op(script_bytes[o] == OP_OUTPUTBYTECODE); o += 1
ensure_op(script_bytes[o] == OP_EQUAL); o += 1
ensure_op(script_bytes[o] == OP_ENDIF); o += 1
ensure(o == script_len, 'Unexpected script length')
ensure(mining_fee >= 700 and mining_fee <= 10000, 'Bad mining_fee')
ensure(len(out_1) == 25, 'Bad out_1')
ensure(len(out_2) == 25 or len(out_2) == 35, 'Bad out_2')
ensure(len(public_key) == 33, 'Bad public_key')
ensure(timelock >= 0, 'Bad timelock')
return mining_fee, out_1, out_2, public_key, timelock
def verifySCLockTx(self, tx_bytes, script_out,
swap_value,
Kal, Kaf,
feerate,
check_lock_tx_inputs, vkbv=None,
**kwargs):
# Verify:
#
# Not necessary to check the lock txn is mineable, as protocol will wait for it to confirm
# However by checking early we can avoid wasting time processing unmineable txns
# Check fee is reasonable
tx = self.loadTx(tx_bytes)
txid = self.getTxid(tx)
self._log.info('Verifying lock tx: {}.'.format(b2h(txid)))
ensure(tx.nVersion == self.txVersion(), 'Bad version')
ensure(tx.nLockTime == 0, 'Bad nLockTime') # TODO match txns created by cores
script_pk = self.getScriptDest(script_out)
locked_n = findOutput(tx, script_pk)
ensure(locked_n is not None, 'Output not found in tx')
locked_coin = tx.vout[locked_n].nValue
# Check value
ensure(locked_coin == swap_value, 'Bad locked value')
# Check script
mining_fee: int = kwargs['mining_fee'] if 'mining_fee' in kwargs else 1000
out_1: bytes = kwargs['out_1']
out_2: bytes = kwargs['out_2']
public_key: bytes = kwargs['public_key'] if 'public_key' in kwargs else Kal
timelock: int = kwargs['timelock']
_mining_fee, _out_1, _out_2, _public_key, _timelock = self.extractScriptLockScriptValues(script_out)
ensure(mining_fee == _mining_fee, 'mining mismatch fee')
ensure(out_1 == _out_1, 'out_1 mismatch')
ensure(out_2 == _out_2, 'out_2 mismatch')
ensure(public_key == _public_key, 'public_key mismatch')
ensure(timelock == _timelock, 'timelock mismatch')
return txid, locked_n
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=None, **kwargs):
# Verify:
# Must have only one input with correct prevout and sequence
# Must have only one output to the p2wsh of the lock refund script
# Output value must be locked_coin - lock tx fee
tx = self.loadTx(tx_bytes)
txid = self.getTxid(tx)
self._log.info('Verifying lock refund tx: {}.'.format(b2h(txid)))
ensure(tx.nVersion == self.txVersion(), 'Bad version')
ensure(tx.nLockTime == 0, 'nLockTime not 0')
ensure(len(tx.vin) == 1, 'tx doesn\'t have one input')
ensure(tx.vin[0].nSequence == prevout_seq, 'Bad input nSequence')
ensure(tx.vin[0].scriptSig == self.getScriptScriptSig(prevout_script, None), 'Input scriptsig mismatch')
ensure(tx.vin[0].prevout.hash == b2i(prevout_id) and tx.vin[0].prevout.n == prevout_n, 'Input prevout mismatch')
ensure(len(tx.vout) == 1, 'tx doesn\'t have one output')
script_pk = self.getScriptDest(script_out)
locked_n = findOutput(tx, script_pk)
ensure(locked_n is not None, 'Output not found in tx')
locked_coin = tx.vout[locked_n].nValue
# Check script
mining_fee: int = kwargs['mining_fee'] if 'mining_fee' in kwargs else 1000
out_1: bytes = kwargs['out_1']
out_2: bytes = kwargs['out_2']
public_key: bytes = kwargs['public_key'] if 'public_key' in kwargs else Kal
timelock: int = kwargs['timelock']
_mining_fee, _out_1, _out_2, _public_key, _timelock = self.extractScriptLockScriptValues(script_out)
ensure(mining_fee == _mining_fee, 'mining mismatch fee')
ensure(out_1 == _out_1, 'out_1 mismatch')
ensure(out_2 == _out_2, 'out_2 mismatch')
ensure(public_key == _public_key, 'public_key mismatch')
ensure(timelock == _timelock, 'timelock mismatch')
fee_paid = locked_coin - mining_fee
assert (fee_paid > 0)
size = self.getTxSize(tx)
vsize = size
self._log.info('tx amount, vsize, fee: %ld, %ld, %ld', locked_coin, vsize, fee_paid)
return txid, locked_coin, locked_n
def verifySCLockRefundSpendTx(self, tx_bytes, lock_refund_tx_bytes,
lock_refund_tx_id, prevout_script,
Kal,
prevout_n, prevout_value, feerate, vkbv=None, **kwargs):
# Verify:
# Must have only one input with correct prevout (n is always 0) and sequence
# Must have only one output sending lock refund tx value - fee to leader's address, TODO: follower shouldn't need to verify destination addr
tx = self.loadTx(tx_bytes)
txid = self.getTxid(tx)
self._log.info('Verifying lock refund spend tx: {}.'.format(b2h(txid)))
ensure(tx.nVersion == self.txVersion(), 'Bad version')
ensure(tx.nLockTime == 0, 'nLockTime not 0')
ensure(len(tx.vin) == 1, 'tx doesn\'t have one input')
ensure(tx.vin[0].nSequence == 0, 'Bad input nSequence')
ensure(tx.vin[0].scriptSig == self.getScriptScriptSig(prevout_script, bytes(73)), 'Input scriptsig mismatch')
ensure(tx.vin[0].prevout.hash == b2i(lock_refund_tx_id) and tx.vin[0].prevout.n == 0, 'Input prevout mismatch')
ensure(len(tx.vout) == 1, 'tx doesn\'t have one output')
# Check script
mining_fee: int = kwargs['mining_fee'] if 'mining_fee' in kwargs else 1000
out_1: bytes = kwargs['out_1']
out_2: bytes = kwargs['out_2']
public_key: bytes = kwargs['public_key'] if 'public_key' in kwargs else Kal
timelock: int = kwargs['timelock']
_mining_fee, _out_1, _out_2, _public_key, _timelock = self.extractScriptLockScriptValues(prevout_script)
ensure(mining_fee == _mining_fee, 'mining mismatch fee')
ensure(out_1 == _out_1, 'out_1 mismatch')
ensure(out_2 == _out_2, 'out_2 mismatch')
ensure(public_key == _public_key, 'public_key mismatch')
ensure(timelock == _timelock, 'timelock mismatch')
tx_value = tx.vout[0].nValue
fee_paid = tx_value - mining_fee
assert (fee_paid > 0)
size = self.getTxSize(tx)
vsize = size
self._log.info('tx amount, vsize, fee: %ld, %ld, %ld', tx_value, vsize, fee_paid)
return True
def verifySCLockSpendTx(self, tx_bytes,
lock_tx_bytes, lock_tx_script,
a_pkhash_f, feerate, vkbv=None):
# Verify:
# Must have only one input with correct prevout (n is always 0) and sequence
# Must have only one output with destination and amount
tx = self.loadTx(tx_bytes)
txid = self.getTxid(tx)
self._log.info('Verifying lock spend tx: {}.'.format(b2h(txid)))
ensure(tx.nVersion == self.txVersion(), 'Bad version')
ensure(tx.nLockTime == 0, 'nLockTime not 0')
ensure(len(tx.vin) == 1, 'tx doesn\'t have one input')
lock_tx = self.loadTx(lock_tx_bytes)
lock_tx_id = self.getTxid(lock_tx)
output_script = self.getScriptDest(lock_tx_script)
locked_n = findOutput(lock_tx, output_script)
ensure(locked_n is not None, 'Output not found in tx')
locked_coin = lock_tx.vout[locked_n].nValue
ensure(tx.vin[0].nSequence == 0, 'Bad input nSequence')
ensure(tx.vin[0].scriptSig == self.getScriptScriptSig(lock_tx_script), 'Input scriptsig mismatch')
# allow for this mismatch in BCH, since the lock txid will get changed after signing
# ensure(tx.vin[0].prevout.hash == b2i(lock_tx_id) and tx.vin[0].prevout.n == locked_n, 'Input prevout mismatch')
ensure(len(tx.vout) == 1, 'tx doesn\'t have one output')
p2pkh = self.getScriptForPubkeyHash(a_pkhash_f)
ensure(tx.vout[0].scriptPubKey == p2pkh, 'Bad output destination')
# The value of the lock tx output should already be verified, if the fee is as expected the difference will be the correct amount
fee_paid = locked_coin - tx.vout[0].nValue
assert (fee_paid > 0)
size = self.getTxSize(tx)
vsize = size
self._log.info('tx amount, vsize, fee: %ld, %ld, %ld', tx.vout[0].nValue, vsize, fee_paid)
return True
def signTxOtVES(self, key_sign: bytes, pubkey_encrypt: bytes, tx_bytes: bytes, input_n: int, prevout_script: bytes, prevout_value: int) -> bytes:
_, out_1, _, _, _ = self.extractScriptLockScriptValues(prevout_script)
msg = sha256(out_1)
return ecdsaotves_enc_sign(key_sign, pubkey_encrypt, msg)
def decryptOtVES(self, k: bytes, esig: bytes) -> bytes:
return ecdsaotves_dec_sig(k, esig)
def recoverEncKey(self, esig, sig, K):
return ecdsaotves_rec_enc_key(K, esig, sig)
def verifyTxOtVES(self, tx_bytes: bytes, ct: bytes, Ks: bytes, Ke: bytes, input_n: int, prevout_script: bytes, prevout_value):
_, out_1, _, _, _ = self.extractScriptLockScriptValues(prevout_script)
msg = sha256(out_1)
return ecdsaotves_enc_verify(Ks, Ke, msg, ct)
def extractLeaderSig(self, tx_bytes: bytes) -> bytes:
tx = self.loadTx(tx_bytes)
signature, _, _, _, _, _ = self.extractScriptLockScriptValuesFromScriptSig(tx.vin[0].scriptSig)
return signature
def extractFollowerSig(self, tx_bytes: bytes) -> bytes:
tx = self.loadTx(tx_bytes)
signature, _, _, _, _, _ = self.extractScriptLockScriptValuesFromScriptSig(tx.vin[0].scriptSig)
return signature
def isSpendingLockTx(self, spend_tx: CTransaction) -> bool:
signature, _, _, _, _, _ = self.extractScriptLockScriptValuesFromScriptSig(spend_tx.vin[0].scriptSig)
return spend_tx.vin[0].nSequence == 0 and signature is not None
def isSpendingLockRefundTx(self, spend_tx: CTransaction) -> bool:
signature, _, _, _, _, _ = self.extractScriptLockScriptValuesFromScriptSig(spend_tx.vin[0].scriptSig)
return spend_tx.vin[0].nSequence == 0 and signature is not None
def isTxExistsError(self, err_str: str) -> bool:
return 'transaction already in block chain' in err_str
def getRefundOutputScript(self, xmr_swap) -> bytes:
_, out_1, _, _, _ = self.extractScriptLockScriptValues(xmr_swap.a_lock_refund_tx_script)
return out_1
def createMercyTx(self, refund_swipe_tx_bytes: bytes, refund_swipe_tx_id: bytes, lock_refund_tx_script: bytes, keyshare: bytes) -> str:
refund_swipe_tx = self.loadTx(refund_swipe_tx_bytes)
refund_output_value = refund_swipe_tx.vout[0].nValue
refund_output_script = refund_swipe_tx.vout[0].scriptPubKey
# mercy transaction size consisting of one input of freshly received funds,
# one op_return with mercy information, a dust output to the leader and change back to the follower
tx_size = 275
dust_limit = 546
outValue = refund_output_value - tx_size - dust_limit
_, out_1, _, _, _ = self.extractScriptLockScriptValues(lock_refund_tx_script)
tx = CTransaction()
tx.nVersion = self.txVersion()
tx.vin.append(CTxIn(COutPoint(b2i(refund_swipe_tx_id), 0),
nSequence=0,
scriptSig=CScript(out_1)))
tx.vout.append(self.txoType()(0, CScript([OP_RETURN, b'XBSW', keyshare])))
tx.vout.append(self.txoType()(dust_limit, CScript(out_1)))
tx.vout.append(self.txoType()(outValue, refund_output_script))
size = tx_size
vsize = size
pay_fee = size
tx.rehash()
self._log.info('createMercyTx %s:\n fee_rate, vsize, fee: %ld, %ld, %ld.',
i2h(tx.sha256), 1, vsize, pay_fee)
txHex = tx.serialize_without_witness()
return self.signTxWithWallet(txHex)

View File

@@ -75,6 +75,7 @@ from basicswap.contrib.test_framework.script import (
OP_CHECKSEQUENCEVERIFY,
OP_DROP,
OP_HASH160, OP_EQUAL,
OP_RETURN,
SIGHASH_ALL,
SegwitV0SignatureHash,
)
@@ -258,6 +259,7 @@ class BTCInterface(Secp256k1Interface):
self._sc = swap_client
self._log = self._sc.log if self._sc and self._sc.log else logging
self._expect_seedid_hex = None
self._altruistic = coin_settings.get('altruistic', True)
def open_rpc(self, wallet=None):
return openrpc(self._rpcport, self._rpcauth, wallet=wallet, host=self._rpc_host)
@@ -601,7 +603,7 @@ class BTCInterface(Secp256k1Interface):
return tx.serialize()
def createSCLockRefundSpendToFTx(self, tx_lock_refund_bytes, script_lock_refund, pkh_dest, tx_fee_rate, vkbv=None):
def createSCLockRefundSpendToFTx(self, tx_lock_refund_bytes, script_lock_refund, pkh_dest, tx_fee_rate, vkbv=None, kbsf=None):
# lock refund swipe tx
# Sends the coinA locked coin to the follower
@@ -625,6 +627,12 @@ class BTCInterface(Secp256k1Interface):
tx.vout.append(self.txoType()(locked_coin, self.getScriptForPubkeyHash(pkh_dest)))
if self.altruistic() and kbsf:
# Add mercy_keyshare
tx.vout.append(self.txoType()(0, CScript([OP_RETURN, b'XBSW', kbsf])))
else:
self._log.debug('Not attaching mercy output, have kbsf {}.'.format('true' if kbsf else 'false'))
dummy_witness_stack = self.getScriptLockRefundSwipeTxDummyWitness(script_lock_refund)
witness_bytes = self.getWitnessStackSerialisedLength(dummy_witness_stack)
vsize = self.getTxVSize(tx, add_witness_bytes=witness_bytes)
@@ -1086,14 +1094,18 @@ class BTCInterface(Secp256k1Interface):
return pay_fee
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, lock_tx_vout=None) -> bytes:
self._log.info('spendBLockTx %s:\n', chain_b_lock_txid.hex())
wtx = self.rpc_wallet('gettransaction', [chain_b_lock_txid.hex(), ])
lock_tx = self.loadTx(bytes.fromhex(wtx['hex']))
self._log.info('spendBLockTx: {} {}\n'.format(chain_b_lock_txid.hex(), lock_tx_vout))
locked_n = lock_tx_vout
Kbs = self.getPubkey(kbs)
script_pk = self.getPkDest(Kbs)
locked_n = findOutput(lock_tx, script_pk)
if locked_n is None:
wtx = self.rpc_wallet('gettransaction', [chain_b_lock_txid.hex(), ])
lock_tx = self.loadTx(bytes.fromhex(wtx['hex']))
locked_n = findOutput(lock_tx, script_pk)
ensure(locked_n is not None, 'Output not found in tx')
pkh_to = self.decodeAddress(address_to)
tx = CTransaction()
@@ -1148,7 +1160,8 @@ class BTCInterface(Secp256k1Interface):
return None
try:
tx = self.rpc_wallet('gettransaction', [txid.hex()])
# set `include_watchonly` explicitly to `True` to get transactions for watchonly addresses also in BCH
tx = self.rpc_wallet('gettransaction', [txid.hex(), True])
block_height = 0
if 'blockhash' in tx:
@@ -1491,6 +1504,18 @@ class BTCInterface(Secp256k1Interface):
'amount': txjs['vout'][n]['value']
}
def inspectSwipeTx(self, tx: dict):
mercy_keyshare = None
for vout in tx['vout']:
script_bytes = bytes.fromhex(vout['scriptPubKey']['hex'])
if len(script_bytes) < 39:
continue
if script_bytes[0] != OP_RETURN:
continue
script_bytes[0]
return script_bytes[7: 7 + 32]
return None
def isTxExistsError(self, err_str: str) -> bool:
return 'Transaction already in block chain' in err_str

View File

@@ -0,0 +1,247 @@
import unittest
CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
def polymod(values):
chk = 1
generator = [
(0x01, 0x98F2BC8E61),
(0x02, 0x79B76D99E2),
(0x04, 0xF33E5FB3C4),
(0x08, 0xAE2EABE2A8),
(0x10, 0x1E4F43E470),
]
for value in values:
top = chk >> 35
chk = ((chk & 0x07FFFFFFFF) << 5) ^ value
for i in generator:
if top & i[0] != 0:
chk ^= i[1]
return chk ^ 1
def calculate_checksum(prefix, payload):
poly = polymod(prefix_expand(prefix) + payload + [0, 0, 0, 0, 0, 0, 0, 0])
out = list()
for i in range(8):
out.append((poly >> 5 * (7 - i)) & 0x1F)
return out
def verify_checksum(prefix, payload):
return polymod(prefix_expand(prefix) + payload) == 0
def b32decode(inputs):
out = list()
for letter in inputs:
out.append(CHARSET.find(letter))
return out
def b32encode(inputs):
out = ""
for char_code in inputs:
out += CHARSET[char_code]
return out
def convertbits(data, frombits, tobits, pad=True):
acc = 0
bits = 0
ret = []
maxv = (1 << tobits) - 1
max_acc = (1 << (frombits + tobits - 1)) - 1
for value in data:
if value < 0 or (value >> frombits):
return None
acc = ((acc << frombits) | value) & max_acc
bits += frombits
while bits >= tobits:
bits -= tobits
ret.append((acc >> bits) & maxv)
if pad:
if bits:
ret.append((acc << (tobits - bits)) & maxv)
elif bits >= frombits or ((acc << (tobits - bits)) & maxv):
return None
return ret
def prefix_expand(prefix):
return [ord(x) & 0x1F for x in prefix] + [0]
class Address:
"""
Class to handle CashAddr.
:param version: Version of CashAddr
:type version: ``str``
:param payload: Payload of CashAddr as int list of the bytearray
:type payload: ``list`` of ``int``
"""
VERSIONS = {
"P2SH20": {"prefix": "bitcoincash", "version_bit": 8, "network": "mainnet"},
"P2SH32": {"prefix": "bitcoincash", "version_bit": 11, "network": "mainnet"},
"P2PKH": {"prefix": "bitcoincash", "version_bit": 0, "network": "mainnet"},
"P2SH20-TESTNET": {"prefix": "bchtest", "version_bit": 8, "network": "testnet"},
"P2SH32-TESTNET": {
"prefix": "bchtest",
"version_bit": 11,
"network": "testnet",
},
"P2PKH-TESTNET": {"prefix": "bchtest", "version_bit": 0, "network": "testnet"},
"P2SH20-REGTEST": {"prefix": "bchreg", "version_bit": 8, "network": "regtest"},
"P2SH32-REGTEST": {"prefix": "bchreg", "version_bit": 11, "network": "regtest"},
"P2PKH-REGTEST": {"prefix": "bchreg", "version_bit": 0, "network": "regtest"},
"P2SH20-CATKN": {
"prefix": "bitcoincash",
"version_bit": 24,
"network": "mainnet",
},
"P2SH32-CATKN": {
"prefix": "bitcoincash",
"version_bit": 27,
"network": "mainnet",
},
"P2PKH-CATKN": {
"prefix": "bitcoincash",
"version_bit": 16,
"network": "mainnet",
},
"P2SH20-CATKN-TESTNET": {
"prefix": "bchtest",
"version_bit": 24,
"network": "testnet",
},
"P2SH32-CATKN-TESTNET": {
"prefix": "bchtest",
"version_bit": 27,
"network": "testnet",
},
"P2PKH-CATKN-TESTNET": {
"prefix": "bchtest",
"version_bit": 16,
"network": "testnet",
},
"P2SH20-CATKN-REGTEST": {
"prefix": "bchreg",
"version_bit": 24,
"network": "regtest",
},
"P2SH32-CATKN-REGTEST": {
"prefix": "bchreg",
"version_bit": 27,
"network": "regtest",
},
"P2PKH-CATKN-REGTEST": {
"prefix": "bchreg",
"version_bit": 16,
"network": "regtest",
},
}
VERSION_SUFFIXES = {"bitcoincash": "", "bchtest": "-TESTNET", "bchreg": "-REGTEST"}
ADDRESS_TYPES = {
0: "P2PKH",
8: "P2SH20",
11: "P2SH32",
16: "P2PKH-CATKN",
24: "P2SH20-CATKN",
27: "P2SH32-CATKN",
}
def __init__(self, version, payload):
if version not in Address.VERSIONS:
raise ValueError("Invalid address version provided")
self.version = version
self.payload = payload
self.prefix = Address.VERSIONS[self.version]["prefix"]
def __str__(self):
return (
f"version: {self.version}\npayload: {self.payload}\nprefix: {self.prefix}"
)
def __repr__(self):
return f"Address('{self.cash_address()}')"
def __eq__(self, other):
if isinstance(other, str):
return self.cash_address() == other
elif isinstance(other, Address):
return self.cash_address() == other.cash_address()
else:
raise ValueError(
"Address can be compared to a string address"
" or an instance of Address"
)
def cash_address(self):
"""
Generate CashAddr of the Address
:rtype: ``str``
"""
version_bit = Address.VERSIONS[self.version]["version_bit"]
payload = [version_bit] + list(self.payload)
payload = convertbits(payload, 8, 5)
checksum = calculate_checksum(self.prefix, payload)
return self.prefix + ":" + b32encode(payload + checksum)
@staticmethod
def from_string(address):
"""
Generate Address from a cashadress string
:param scriptcode: The cashaddress string
:type scriptcode: ``str``
:returns: Instance of :class:~bitcash.cashaddress.Address
"""
try:
address = str(address)
except Exception:
raise ValueError("Expected string as input")
if address.upper() != address and address.lower() != address:
raise ValueError(
"Cash address contains uppercase and lowercase characters: " + address
)
address = address.lower()
colon_count = address.count(":")
if colon_count == 0:
raise ValueError("Cash address is missing prefix")
if colon_count > 1:
raise ValueError("Cash address contains more than one colon character")
prefix, base32string = address.split(":")
decoded = b32decode(base32string)
if not verify_checksum(prefix, decoded):
raise ValueError(
"Bad cash address checksum for address {}".format(address)
)
converted = convertbits(decoded, 5, 8)
try:
version = Address.ADDRESS_TYPES[converted[0]]
except Exception:
raise ValueError("Could not determine address version")
version += Address.VERSION_SUFFIXES[prefix]
payload = converted[1:-6]
return Address(version, payload)
class TestFrameworkScript(unittest.TestCase):
def test_base58encodedecode(self):
def check_cashaddress(address: str):
self.assertEqual(Address.from_string(address).cash_address(), address)
check_cashaddress("bitcoincash:qzfyvx77v2pmgc0vulwlfkl3uzjgh5gnmqk5hhyaa6")

View File

@@ -0,0 +1,43 @@
# -*- 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.
from basicswap.contrib.test_framework.script import CScriptOp
OP_TXINPUTCOUNT = CScriptOp(0xc3)
OP_1 = CScriptOp(0x51)
OP_NUMEQUALVERIFY = CScriptOp(0x9d)
OP_TXOUTPUTCOUNT = CScriptOp(0xc4)
OP_0 = CScriptOp(0x00)
OP_UTXOVALUE = CScriptOp(0xc6)
OP_OUTPUTVALUE = CScriptOp(0xcc)
OP_SUB = CScriptOp(0x94)
OP_UTXOTOKENCATEGORY = CScriptOp(0xce)
OP_OUTPUTTOKENCATEGORY = CScriptOp(0xd1)
OP_EQUALVERIFY = CScriptOp(0x88)
OP_UTXOTOKENCOMMITMENT = CScriptOp(0xcf)
OP_OUTPUTTOKENCOMMITMENT = CScriptOp(0xd2)
OP_UTXOTOKENAMOUNT = CScriptOp(0xd0)
OP_OUTPUTTOKENAMOUNT = CScriptOp(0xd3)
OP_INPUTSEQUENCENUMBER = CScriptOp(0xcb)
OP_NOTIF = CScriptOp(0x64)
OP_OUTPUTBYTECODE = CScriptOp(0xcd)
OP_OVER = CScriptOp(0x78)
OP_CHECKDATASIG = CScriptOp(0xba)
OP_CHECKDATASIGVERIFY = CScriptOp(0xbb)
OP_ELSE = CScriptOp(0x67)
OP_CHECKSEQUENCEVERIFY = CScriptOp(0xb2)
OP_DROP = CScriptOp(0x75)
OP_EQUAL = CScriptOp(0x87)
OP_ENDIF = CScriptOp(0x68)
OP_HASH256 = CScriptOp(0xaa)
OP_PUSHBYTES_32 = CScriptOp(0x20)
OP_DUP = CScriptOp(0x76)
OP_HASH160 = CScriptOp(0xa9)
OP_CHECKSIG = CScriptOp(0xac)
OP_SHA256 = CScriptOp(0xa8)
OP_VERIFY = CScriptOp(0x69)

View File

@@ -262,6 +262,7 @@ class DCRInterface(Secp256k1Interface):
self._use_segwit = True # Decred is natively segwit
self._connection_type = coin_settings['connection_type']
self._altruistic = coin_settings.get('altruistic', True)
def open_rpc(self):
return openrpc(self._rpcport, self._rpcauth, host=self._rpc_host)
@@ -1232,7 +1233,7 @@ class DCRInterface(Secp256k1Interface):
return True
def createSCLockRefundSpendToFTx(self, tx_lock_refund_bytes, script_lock_refund, pkh_dest, tx_fee_rate, vkbv=None):
def createSCLockRefundSpendToFTx(self, tx_lock_refund_bytes, script_lock_refund, pkh_dest, tx_fee_rate, vkbv=None, kbsf=None):
# lock refund swipe tx
# Sends the coinA locked coin to the follower

View File

@@ -42,7 +42,7 @@ class FIROInterface(BTCInterface):
# No multiwallet support
self.rpc_wallet = make_rpc_func(self._rpcport, self._rpcauth, host=self._rpc_host)
def getExchangeName(self, exchange_name):
def getExchangeName(self, exchange_name: str) -> str:
return 'zcoin'
def initialiseWallet(self, key):

View File

@@ -95,8 +95,6 @@ class LTCInterfaceMWEB(LTCInterface):
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:

View File

@@ -668,7 +668,7 @@ class NAVInterface(BTCInterface):
return tx.serialize()
def createSCLockRefundSpendToFTx(self, tx_lock_refund_bytes, script_lock_refund, pkh_dest, tx_fee_rate, vkbv=None):
def createSCLockRefundSpendToFTx(self, tx_lock_refund_bytes, script_lock_refund, pkh_dest, tx_fee_rate, vkbv=None, kbsf=None):
# lock refund swipe tx
# Sends the coinA locked coin to the follower

View File

@@ -622,7 +622,7 @@ class PARTInterfaceBlind(PARTInterface):
return True
def createSCLockRefundSpendToFTx(self, tx_lock_refund_bytes, script_lock_refund, pkh_dest, tx_fee_rate, vkbv):
def createSCLockRefundSpendToFTx(self, tx_lock_refund_bytes, script_lock_refund, pkh_dest, tx_fee_rate, vkbv, kbsf=None):
# lock refund swipe tx
# Sends the coinA locked coin to the follower
lock_refund_tx_obj = self.rpc('decoderawtransaction', [tx_lock_refund_bytes.hex()])

View File

@@ -364,7 +364,8 @@ def js_bids(self, url_split, post_string: str, is_json: bool) -> bytes:
bid_id = bytes.fromhex(url_split[3])
assert (len(bid_id) == 28)
show_txns = False
show_txns: bool = False
with_events: bool = False
if post_string != '':
post_data = getFormData(post_string, is_json)
if have_data_entry(post_data, 'accept'):
@@ -376,6 +377,8 @@ def js_bids(self, url_split, post_string: str, is_json: bool) -> bytes:
if have_data_entry(post_data, 'show_extra'):
show_txns = True
if have_data_entry(post_data, 'with_events'):
with_events = True
bid, xmr_swap, offer, xmr_offer, events = swap_client.getXmrBidAndOffer(bid_id)
assert (bid), 'Unknown bid ID'
@@ -389,6 +392,17 @@ def js_bids(self, url_split, post_string: str, is_json: bool) -> bytes:
if len(url_split) > 4 and url_split[4] == 'states':
old_states = listOldBidStates(bid)
if with_events:
new_list = []
for entry in old_states:
entry_list = list(entry)
entry_list.insert(1, 'state')
new_list.append(entry_list)
for event in events:
new_list.append([event['at'], 'event', event['desc']])
old_states = sorted(new_list, key=lambda x: x[0])
return bytes(json.dumps(old_states), 'UTF-8')
edit_bid = False

View File

@@ -163,7 +163,11 @@ def setDLEAG(xmr_swap, ci_to, kbsf: bytes) -> None:
class XmrSwapInterface(ProtocolInterface):
swap_type = SwapTypes.XMR_SWAP
def genScriptLockTxScript(self, ci, Kal: bytes, Kaf: bytes) -> CScript:
def genScriptLockTxScript(self, ci, Kal: bytes, Kaf: bytes, **kwargs) -> CScript:
# fallthrough to ci if genScriptLockTxScript is implemented there
if hasattr(ci, 'genScriptLockTxScript') and callable(ci.genScriptLockTxScript):
return ci.genScriptLockTxScript(ci, Kal, Kaf, **kwargs)
Kal_enc = Kal if len(Kal) == 33 else ci.encodePubkey(Kal)
Kaf_enc = Kaf if len(Kaf) == 33 else ci.encodePubkey(Kaf)

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

@@ -15,6 +15,7 @@ const coinNameToSymbol = {
'Decred': 'decred',
'Zano': 'zano',
'Dogecoin': 'dogecoin',
'Bitcoin Cash': 'bitcoincash'
};
function makePostRequest(url, headers = {}) {
@@ -166,7 +167,7 @@ const itemsPerPage = 100;
const coinIdToName = {
1: 'particl', 2: 'bitcoin', 3: 'litecoin', 4: 'decred',
6: 'monero', 7: 'particl blind', 8: 'particl anon',
9: 'wownero', 11: 'pivx', 13: 'firo'
9: 'wownero', 11: 'pivx', 13: 'firo', 17: 'bitcoincash'
};
// DOM

View File

@@ -409,7 +409,7 @@
document.getElementById('get_rate_inferred_button').addEventListener('click', getRateInferred);
function set_swap_type_enabled(coin_from, coin_to, swap_type) {
const adaptor_sig_only_coins = ['6' /* XMR */,'9' /* WOW */, '8' /* PART_ANON */, '7' /* PART_BLIND */, '13' /* FIRO */];
const adaptor_sig_only_coins = ['6' /* XMR */,'9' /* WOW */, '8' /* PART_ANON */, '7' /* PART_BLIND */, '13' /* FIRO */, '17' /* BCH */];
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)) {

View File

@@ -878,6 +878,7 @@ const coinNameToSymbol = {
'PIVX': 'PIVX',
'Decred': 'DCR',
'Zano': 'ZANO',
'Bitcoin Cash': 'BCH',
};
const getUsdValue = (cryptoValue, coinSymbol) => {

View File

@@ -257,6 +257,7 @@ const coinNameToSymbol = {
'PIVX': 'PIVX',
'Decred': 'DCR',
'Zano': 'ZANO',
'Bitcoin Cash': 'BCH',
};
const getUsdValue = async (cryptoValue, coinSymbol) => {

View File

@@ -161,7 +161,7 @@ def parseOfferFormData(swap_client, form_data, page_data, options={}):
page_data['swap_type'] = get_data_entry(form_data, 'swap_type')
parsed_data['swap_type'] = page_data['swap_type']
swap_type = swap_type_from_string(parsed_data['swap_type'])
elif parsed_data['coin_from'] in swap_client.scriptless_coins or parsed_data['coin_to'] in swap_client.scriptless_coins:
elif parsed_data['coin_from'] in swap_client.adaptor_swap_only_coins or parsed_data['coin_to'] in swap_client.adaptor_swap_only_coins:
parsed_data['swap_type'] = strSwapType(SwapTypes.XMR_SWAP)
swap_type = SwapTypes.XMR_SWAP
else:
@@ -224,7 +224,7 @@ def parseOfferFormData(swap_client, form_data, page_data, options={}):
try:
if len(errors) == 0 and page_data['swap_style'] == 'xmr':
reverse_bid: bool = swap_client.is_reverse_ads_bid(coin_from)
reverse_bid: bool = swap_client.is_reverse_ads_bid(coin_from, coin_to)
ci_leader = ci_to if reverse_bid else ci_from
ci_follower = ci_from if reverse_bid else ci_to

View File

@@ -7,6 +7,7 @@
import json
import struct
from basicswap.util import (
hex_or_none,
make_int,
format_timestamp,
)
@@ -157,7 +158,7 @@ def describeBid(swap_client, bid, xmr_swap, offer, xmr_offer, bid_events, edit_b
ci_from = swap_client.ci(Coins(offer.coin_from))
ci_to = swap_client.ci(Coins(offer.coin_to))
reverse_bid: bool = swap_client.is_reverse_ads_bid(offer.coin_from)
reverse_bid: bool = swap_client.is_reverse_ads_bid(offer.coin_from, offer.coin_to)
ci_leader = ci_to if reverse_bid else ci_from
ci_follower = ci_from if reverse_bid else ci_to
@@ -303,7 +304,7 @@ def describeBid(swap_client, bid, xmr_swap, offer, xmr_offer, bid_events, edit_b
confirms = None
if swap_client.coin_clients[ci_leader.coin_type()]['chain_height'] and bid.xmr_a_lock_tx.chain_height:
confirms = (swap_client.coin_clients[ci_leader.coin_type()]['chain_height'] - bid.xmr_a_lock_tx.chain_height) + 1
txns.append({'type': 'Chain A Lock', 'txid': bid.xmr_a_lock_tx.txid.hex(), 'confirms': confirms})
txns.append({'type': 'Chain A Lock', 'txid': hex_or_none(bid.xmr_a_lock_tx.txid), 'confirms': confirms})
if bid.xmr_a_lock_spend_tx:
txns.append({'type': 'Chain A Lock Spend', 'txid': bid.xmr_a_lock_spend_tx.txid.hex()})
if bid.xmr_b_lock_tx:
@@ -435,8 +436,8 @@ def getCoinName(c):
return chainparams[Coins.LTC]['name'].capitalize() + ' MWEB'
coin_chainparams = chainparams[c]
if coin_chainparams.get('use_ticker_as_name', False):
return coin_chainparams['ticker']
if 'display_name' in coin_chainparams:
return coin_chainparams['display_name']
return coin_chainparams['name'].capitalize()

View File

@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2018-2023 tecnovert
# Copyright (c) 2024 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -215,3 +216,9 @@ def zeroIfNone(value) -> int:
if value is None:
return 0
return value
def hex_or_none(value: bytes) -> str:
if value is None:
return 'None'
return value.hex()

View File

@@ -6,6 +6,7 @@
import struct
import hashlib
from basicswap.contrib.test_framework.script import OP_PUSHDATA1, OP_PUSHDATA2, OP_PUSHDATA4, CScriptInvalidError, CScriptTruncatedPushDataError
from basicswap.script import OpCodes
@@ -32,6 +33,51 @@ def decodeScriptNum(script_bytes, o):
return (v, 1 + num_len)
def decodePushData(script_bytes, o):
datasize = None
pushdata_type = None
i = o
opcode = script_bytes[i]
i += 1
if opcode < OP_PUSHDATA1:
pushdata_type = 'PUSHDATA(%d)' % opcode
datasize = opcode
elif opcode == OP_PUSHDATA1:
pushdata_type = 'PUSHDATA1'
if i >= len(script_bytes):
raise CScriptInvalidError('PUSHDATA1: missing data length')
datasize = script_bytes[i]
i += 1
elif opcode == OP_PUSHDATA2:
pushdata_type = 'PUSHDATA2'
if i + 1 >= len(script_bytes):
raise CScriptInvalidError('PUSHDATA2: missing data length')
datasize = script_bytes[i] + (script_bytes[i + 1] << 8)
i += 2
elif opcode == OP_PUSHDATA4:
pushdata_type = 'PUSHDATA4'
if i + 3 >= len(script_bytes):
raise CScriptInvalidError('PUSHDATA4: missing data length')
datasize = script_bytes[i] + (script_bytes[i + 1] << 8) + (script_bytes[i + 2] << 16) + (script_bytes[i + 3] << 24)
i += 4
else:
assert False # shouldn't happen
data = bytes(script_bytes[i:i + datasize])
# Check for truncation
if len(data) < datasize:
raise CScriptTruncatedPushDataError('%s: truncated data' % pushdata_type, data)
# return data and the number of bytes to skip forward
return (data, i + datasize - o)
def getP2SHScriptForHash(p2sh):
return bytes((OpCodes.OP_HASH160, 0x14)) \
+ p2sh \

View File

@@ -1,3 +1,13 @@
0.14.2
==============
- BCH support.
- Mercy outputs on swipe txns of BTC descended coins.
- Disable by setting 'altruistic' to false in basicswap.json
0.13.2
==============

View File

@@ -0,0 +1,27 @@
# https://github.com/NicolasDorier/docker-bitcoin/blob/master/README.md
FROM i_swapclient as install_stage
RUN basicswap-prepare --preparebinonly --bindir=/coin_bin --withcoin=bitcoincash --withoutcoins=particl && \
find /coin_bin -name *.tar.gz -delete
FROM debian:bullseye-slim
COPY --from=install_stage /coin_bin .
ENV BITCOINCASH_DATA /data
RUN groupadd -r bitcoincash && useradd -r -m -g bitcoincash bitcoincash \
&& apt-get update \
&& apt-get install -qq --no-install-recommends gosu \
&& rm -rf /var/lib/apt/lists/* \
&& mkdir "$BITCOINCASH_DATA" \
&& chown -R bitcoincash:bitcoincash "$BITCOINCASH_DATA" \
&& ln -sfn "$BITCOINCASH_DATA" /home/bitcoincash/.bitcoincash \
&& chown -h bitcoincash:bitcoincash /home/bitcoincash/.bitcoincash
VOLUME /data
COPY entrypoint.sh /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
EXPOSE 8332 8333 18332 18333 18443 18444
CMD ["/bitcoincash/bitcoind", "--datadir=/data"]

View File

@@ -0,0 +1,11 @@
#!/bin/bash
set -e
if [[ "$1" == "bitcoin-cli" || "$1" == "bitcoin-tx" || "$1" == "bitcoind" || "$1" == "test_bitcoin" ]]; then
mkdir -p "$BITCOINCASH_DATA"
chown -h bitcoincash:bitcoincash /home/bitcoincash/.bitcoincash
exec gosu bitcoincash "$@"
else
exec "$@"
fi

View File

@@ -0,0 +1,16 @@
bitcoincash_core:
image: i_bitcoincash
build:
context: bitcoincash
dockerfile: Dockerfile
container_name: bitcoincash_core
volumes:
- ${DATA_PATH}/bitcoincash:/data
expose:
- ${BCH_RPC_PORT}
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
restart: unless-stopped

View File

@@ -7,7 +7,7 @@
volumes:
- ${DATA_PATH}/wownero_daemon:/data
expose:
- ${BASE_WOW_RPC_PORT}
- ${WOW_RPC_PORT}
logging:
driver: "json-file"
options:

View File

@@ -16,6 +16,7 @@
- ${DATA_PATH}/pivx:/data/pivx
- ${DATA_PATH}/dash:/data/dash
- ${DATA_PATH}/firo:/data/firo
- ${DATA_PATH}/bitcoincash:/data/bitcoincash
environment:
- TZ
- BSX_DOCKER_MODE
@@ -50,8 +51,8 @@
- DEFAULT_XMR_RESTORE_HEIGHT
- WOW_DATA_DIR
- WOW_RPC_HOST
- BASE_WOW_RPC_PORT
- BASE_WOW_ZMQ_PORT
- WOW_RPC_PORT
- WOW_ZMQ_PORT
- WOW_WALLETS_DIR
- WOW_WALLET_RPC_HOST
- WOW_WALLET_RPC_PORT
@@ -73,4 +74,9 @@
- FIRO_RPC_PORT
- FIRO_RPC_USER
- FIRO_RPC_PWD
- BCH_DATA_DIR
- BCH_RPC_HOST
- BCH_RPC_PORT
- BCH_RPC_USER
- BCH_RPC_PWD
restart: "no"

View File

@@ -72,3 +72,9 @@ FIRO_RPC_HOST=firo_core
FIRO_RPC_PORT=8888
FIRO_RPC_USER=firo_user
FIRO_RPC_PWD=firo_pwd
BCH_DATA_DIR=/data/bitcoincash
BCH_RPC_HOST=bitcoincash_core
BCH_RPC_PORT=19797
BCH_RPC_USER=bitcoincash_user
BCH_RPC_PWD=bitcoincash_pwd

View File

@@ -165,7 +165,7 @@ Prepare config files:
export ADD_COIN=monero
docker-compose -f docker-compose-prepare.yml run --rm swapprepare \
basicswap-prepare --nocores --usecontainers --addcoin=${ADD_COIN} --htmlhost="0.0.0.0" --particl_mnemonic=none
basicswap-prepare --nocores --usecontainers --addcoin=${ADD_COIN} --particl_mnemonic=none
Prepare wallet:
@@ -177,7 +177,7 @@ Prepare wallet:
docker-compose -f docker-compose-prepare.yml run -e WALLET_ENCRYPTION_PWD=walletpass \
--rm swapprepare \
basicswap-prepare --initwalletsonly --withoutcoin=particl --withcoin=monero
basicswap-prepare --initwalletsonly --withoutcoin=particl --withcoin=${ADD_COIN}
docker-compose -f docker-compose-prepare.yml stop

View File

@@ -6,7 +6,7 @@ ENV LANG=C.UTF-8 \
RUN apt-get update; \
apt-get install -y --no-install-recommends \
python3-pip libpython3-dev gnupg pkg-config gcc libc-dev gosu tzdata;
python3-pip libpython3-dev gnupg pkg-config gcc libc-dev gosu tzdata wget unzip;
ARG BASICSWAP_URL=https://github.com/basicswap/basicswap/archive/master.zip
ARG BASICSWAP_DIR=basicswap-master

View File

@@ -35,10 +35,6 @@ LTC_BASE_PORT = 34792
LTC_BASE_RPC_PORT = 35792
LTC_BASE_ZMQ_PORT = 36792
DCR_BASE_PORT = 18555
DCR_BASE_RPC_PORT = 9110
PIVX_BASE_PORT = 34892
PIVX_BASE_RPC_PORT = 35892
PIVX_BASE_ZMQ_PORT = 36892
@@ -46,7 +42,7 @@ PIVX_BASE_ZMQ_PORT = 36892
PREFIX_SECRET_KEY_REGTEST = 0x2e
def prepareDataDir(datadir, node_id, conf_file, dir_prefix, base_p2p_port=BASE_PORT, base_rpc_port=BASE_RPC_PORT, num_nodes=3):
def prepareDataDir(datadir, node_id, conf_file, dir_prefix, base_p2p_port=BASE_PORT, base_rpc_port=BASE_RPC_PORT, num_nodes=3, extra_opts=[]):
node_dir = os.path.join(datadir, dir_prefix + str(node_id))
if not os.path.exists(node_dir):
os.makedirs(node_dir)
@@ -75,8 +71,12 @@ def prepareDataDir(datadir, node_id, conf_file, dir_prefix, base_p2p_port=BASE_P
fp.write('acceptnonstdtxn=0\n')
fp.write('txindex=1\n')
fp.write('wallet=wallet.dat\n')
fp.write('findpeers=0\n')
for opt in extra_opts:
fp.write(opt + '\n')
if base_p2p_port == BTC_BASE_PORT:
fp.write('deprecatedrpc=create_bdb\n')
elif base_p2p_port == BASE_PORT: # Particl
@@ -141,7 +141,7 @@ def wait_for_bid(delay_event, swap_client, bid_id, state=None, sent: bool = Fals
assert (len(bids) < 2)
for bid in bids:
if bid[2] == bid_id:
if isinstance(state, list):
if isinstance(state, (list, tuple)):
if bid[5] in state:
return
else:

View File

@@ -29,9 +29,15 @@ from tests.basicswap.common import (
BASE_PORT, BASE_RPC_PORT,
BTC_BASE_PORT, BTC_BASE_RPC_PORT, BTC_BASE_TOR_PORT,
LTC_BASE_PORT, LTC_BASE_RPC_PORT,
DCR_BASE_PORT, DCR_BASE_RPC_PORT,
PIVX_BASE_PORT,
)
from tests.basicswap.extended.test_dcr import (
DCR_BASE_PORT, DCR_BASE_RPC_PORT,
)
from tests.basicswap.test_bch_xmr import (
BCH_BASE_PORT, BCH_BASE_RPC_PORT,
)
from basicswap.contrib.rpcauth import generate_salt, password_to_hmac
import basicswap.config as cfg
@@ -48,6 +54,8 @@ 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))
DECRED_RPC_PORT_BASE = int(os.getenv('DECRED_RPC_PORT_BASE', DCR_BASE_RPC_PORT))
BITCOINCASH_RPC_PORT_BASE = int(os.getenv('BITCOINCASH_RPC_PORT_BASE', BCH_BASE_RPC_PORT))
FIRO_BASE_PORT = 34832
FIRO_BASE_RPC_PORT = 35832
@@ -97,6 +105,8 @@ def run_prepare(node_id, datadir_path, bins_path, with_coins, mnemonic_in=None,
os.environ['BTC_RPC_PORT'] = str(BITCOIN_RPC_PORT_BASE)
os.environ['LTC_RPC_PORT'] = str(LITECOIN_RPC_PORT_BASE)
os.environ['DCR_RPC_PORT'] = str(DECRED_RPC_PORT_BASE)
os.environ['BCH_PORT'] = str(BCH_BASE_PORT)
os.environ['BCH_RPC_PORT'] = str(BITCOINCASH_RPC_PORT_BASE)
os.environ['FIRO_RPC_PORT'] = str(FIRO_RPC_PORT_BASE)
os.environ['XMR_RPC_USER'] = 'xmr_user'
@@ -117,6 +127,7 @@ def run_prepare(node_id, datadir_path, bins_path, with_coins, mnemonic_in=None,
'-regtest',
f'-withcoins={with_coins}',
'-noextractover',
'-noreleasesizecheck',
'-xmrrestoreheight=0']
if mnemonic_in:
testargs.append(f'-particl_mnemonic="{mnemonic_in}"')
@@ -311,6 +322,33 @@ def run_prepare(node_id, datadir_path, bins_path, with_coins, mnemonic_in=None,
if ip != node_id:
fp.write('add-exclusive-node=127.0.0.1:{}\n'.format(XMR_BASE_P2P_PORT + ip + port_ofs))
if 'bitcoincash' in coins_array:
config_filename = os.path.join(datadir_path, 'bitcoincash', 'bitcoin.conf')
with open(config_filename, 'r') as fp:
lines = fp.readlines()
with open(config_filename, 'w') as fp:
for line in lines:
if not line.startswith('prune'):
fp.write(line)
# NOTE: port is set (when starting daemon) from basicswap.json
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_bch_' + str(node_id)
rpc_pass = 'test_bch_pwd_' + str(node_id)
fp.write('rpcauth={}:{}${}\n'.format(rpc_user, salt, password_to_hmac(salt, rpc_pass)))
settings['chainclients']['bitcoincash']['rpcuser'] = rpc_user
settings['chainclients']['bitcoincash']['rpcpassword'] = rpc_pass
for ip in range(num_nodes):
if ip != node_id:
fp.write('connect=127.0.0.1:{}\n'.format(BCH_BASE_PORT + ip + port_ofs))
for opt in EXTRA_CONFIG_JSON.get('bch{}'.format(node_id), []):
fp.write(opt + '\n')
with open(config_path) as fs:
settings = json.load(fs)

View File

@@ -44,6 +44,9 @@ from tests.basicswap.common import (
BTC_BASE_RPC_PORT,
LTC_BASE_RPC_PORT,
)
from tests.basicswap.test_bch_xmr import (
BCH_BASE_RPC_PORT,
)
from tests.basicswap.util import (
make_boolean,
read_json_api,
@@ -66,6 +69,7 @@ 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))
BITCOINCASH_RPC_PORT_BASE = int(os.getenv('BITCOINCASH_RPC_PORT_BASE', BCH_BASE_RPC_PORT))
DECRED_WALLET_RPC_PORT_BASE = int(os.getenv('DECRED_WALLET_RPC_PORT_BASE', 9210))
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')
@@ -99,6 +103,11 @@ def calldcrrpc(node_id, method, params=[], wallet=None, base_rpc_port=DECRED_WAL
return callrpc_dcr(base_rpc_port + node_id, auth, method, params)
def callbchrpc(node_id, method, params=[], wallet=None, base_rpc_port=BITCOINCASH_RPC_PORT_BASE + PORT_OFS):
auth = 'test_bch_{0}:test_bch_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:
@@ -106,6 +115,8 @@ def updateThread(cls):
callbtcrpc(0, 'generatetoaddress', [1, cls.btc_addr])
if cls.ltc_addr is not None:
callltcrpc(0, 'generatetoaddress', [1, cls.ltc_addr])
if cls.bch_addr is not None:
callbchrpc(0, 'generatetoaddress', [1, cls.bch_addr])
except Exception as e:
print('updateThread error', str(e))
cls.delay_event.wait(random.randrange(cls.update_min, cls.update_max))
@@ -174,6 +185,7 @@ class Test(unittest.TestCase):
cls.processes = []
cls.btc_addr = None
cls.ltc_addr = None
cls.bch_addr = None
cls.xmr_addr = None
cls.dcr_addr = 'SsYbXyjkKAEXXcGdFgr4u4bo4L8RkCxwQpH'
cls.dcr_acc = None
@@ -257,6 +269,14 @@ class Test(unittest.TestCase):
self.update_thread_dcr = threading.Thread(target=updateThreadDCR, args=(self,))
self.update_thread_dcr.start()
if 'bitcoincash' in TEST_COINS_LIST:
self.bch_addr = callbchrpc(0, 'getnewaddress', ['mining_addr'], wallet='wallet.dat')
num_blocks: int = 200
have_blocks: int = callbchrpc(0, 'getblockcount')
if have_blocks < num_blocks:
logging.info('Mining %d Bitcoincash blocks to %s', num_blocks - have_blocks, self.bch_addr)
callbchrpc(0, 'generatetoaddress', [num_blocks - have_blocks, self.bch_addr], wallet='wallet.dat')
if RESET_TEST:
# Lower output split threshold for more stakeable outputs
for i in range(NUM_NODES):

View File

@@ -0,0 +1,624 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright (c) 2024 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
import logging
import os
import random
import unittest
import basicswap.config as cfg
from basicswap.basicswap import (
Coins,
SwapTypes,
)
from basicswap.bin.run import startDaemon
from basicswap.util.crypto import sha256
from tests.basicswap.test_btc_xmr import BasicSwapTest
from tests.basicswap.common import (
make_rpc_func,
prepareDataDir,
stopDaemons,
waitForRPC,
)
from basicswap.contrib.test_framework.messages import (
ToHex,
CTxIn,
COutPoint,
CTransaction,
)
from basicswap.contrib.test_framework.script import (
CScript,
OP_EQUAL,
OP_CHECKLOCKTIMEVERIFY,
OP_CHECKSEQUENCEVERIFY,
)
from basicswap.interface.bch import BCHInterface
from basicswap.rpc import (
callrpc_cli,
)
from basicswap.util import ensure
from .test_xmr import test_delay_event, callnoderpc
from coincurve.ecdsaotves import (
ecdsaotves_enc_sign,
ecdsaotves_enc_verify,
ecdsaotves_dec_sig
)
BITCOINCASH_BINDIR = os.path.expanduser(os.getenv('BITCOINCASH_BINDIR', os.path.join(cfg.DEFAULT_TEST_BINDIR, 'bitcoincash')))
BITCOINCASHD = os.getenv('BITCOINCASHD', 'bitcoind' + cfg.bin_suffix)
BITCOINCASH_CLI = os.getenv('BITCOINCASH_CLI', 'bitcoin-cli' + cfg.bin_suffix)
BITCOINCASH_TX = os.getenv('BITCOINCASH_TX', 'bitcoin-tx' + cfg.bin_suffix)
BCH_BASE_PORT = 41792
BCH_BASE_RPC_PORT = 42792
BCH_BASE_ZMQ_PORT = 43792
BCH_BASE_TOR_PORT = 43732
logger = logging.getLogger()
bch_lock_spend_tx = '0200000001bfc6bbb47851441c7827059ae337a06aa9064da7f9537eb9243e45766c3dd34c00000000d8473045022100a0161ea14d3b41ed41250c8474fc8ec6ce1cab8df7f401e69ecf77c2ab63d82102207a2a57ddf2ea400e09ea059f3b261da96f5098858b17239931f3cc2fb929bb2a4c8ec3519dc4519d02e80300c600cc949d00ce00d18800cf00d28800d000d39d00cb641976a91481ec21969399d15c26af089d5db437ead066c5ba88ac00cd788821024ffcc0481629866671d89f05f3da813a2aacec1b52e69b8c0c586b665f5d4574ba6752b27523aa20df65a90e9becc316ff5aca44d4e06dfaade56622f32bafa197aba706c5e589758700cd87680000000001251cde06000000001976a91481ec21969399d15c26af089d5db437ead066c5ba88ac00000000'
bch_lock_script = 'c3519dc4519d02e80300c600cc949d00ce00d18800cf00d28800d000d39d00cb641976a91481ec21969399d15c26af089d5db437ead066c5ba88ac00cd788821024ffcc0481629866671d89f05f3da813a2aacec1b52e69b8c0c586b665f5d4574ba6752b27523aa20df65a90e9becc316ff5aca44d4e06dfaade56622f32bafa197aba706c5e589758700cd8768'
bch_lock_spend_script = '473045022100a0161ea14d3b41ed41250c8474fc8ec6ce1cab8df7f401e69ecf77c2ab63d82102207a2a57ddf2ea400e09ea059f3b261da96f5098858b17239931f3cc2fb929bb2a4c8ec3519dc4519d02e80300c600cc949d00ce00d18800cf00d28800d000d39d00cb641976a91481ec21969399d15c26af089d5db437ead066c5ba88ac00cd788821024ffcc0481629866671d89f05f3da813a2aacec1b52e69b8c0c586b665f5d4574ba6752b27523aa20df65a90e9becc316ff5aca44d4e06dfaade56622f32bafa197aba706c5e589758700cd8768'
bch_lock_swipe_script = '4c8fc3519dc4519d02e80300c600cc949d00ce00d18800cf00d28800d000d39d00cb641976a9141ab50aedd2e48297073f0f6eef46f97b37c9354e88ac00cd7888210234fe304a5b129b8265c177c92aa40b7840e8303f8b0fcca2359023163c7c2768ba670120b27523aa20191b09e40d1277fa14fea1e9b41e4fcc4528c9cb77e39e1b7b1a0b3332180cb78700cd8768'
coin_settings = {'rpcport': 0, 'rpcauth': 'none', 'blocks_confirmed': 1, 'conf_target': 1, 'use_segwit': False, 'connection_type': 'rpc'}
class TestXmrBchSwapInterface(unittest.TestCase):
def test_extractScriptLockScriptValues(self):
ci = BCHInterface(coin_settings, "regtest")
script_bytes = CScript(bytes.fromhex(bch_lock_script))
ci.extractScriptLockScriptValues(script_bytes)
script_bytes = CScript(bytes.fromhex(bch_lock_spend_script))
signature, mining_fee, out_1, out_2, public_key, timelock = ci.extractScriptLockScriptValuesFromScriptSig(script_bytes)
ensure(signature is not None, 'signature not present')
script_bytes = CScript(bytes.fromhex(bch_lock_swipe_script))
signature, mining_fee, out_1, out_2, public_key, timelock = ci.extractScriptLockScriptValuesFromScriptSig(script_bytes)
ensure(signature is None, 'signature present')
class TestBCH(BasicSwapTest):
__test__ = True
test_coin = Coins.BCH
test_coin_from = Coins.BCH
base_rpc_port = BCH_BASE_RPC_PORT
bch_daemons = []
start_ltc_nodes = False
bch_addr = None
@classmethod
def prepareExtraDataDir(cls, i):
if not cls.restore_instance:
data_dir = prepareDataDir(cfg.TEST_DATADIRS, i, 'bitcoin.conf', 'bch_', base_p2p_port=BCH_BASE_PORT, base_rpc_port=BCH_BASE_RPC_PORT)
# Rewrite conf file
config_filename: str = os.path.join(cfg.TEST_DATADIRS, 'bch_' + str(i), 'bitcoin.conf')
with open(config_filename, 'r') as fp:
lines = fp.readlines()
with open(config_filename, 'w') as fp:
for line in lines:
if not line.startswith('findpeers'):
fp.write(line)
if os.path.exists(os.path.join(BITCOINCASH_BINDIR, 'bitcoin-wallet')):
try:
callrpc_cli(BITCOINCASH_BINDIR, data_dir, 'regtest', '-wallet=wallet.dat create', 'bitcoin-wallet')
except Exception as e:
logging.warning('bch: bitcoin-wallet create failed')
raise e
cls.bch_daemons.append(startDaemon(os.path.join(cfg.TEST_DATADIRS, 'bch_' + str(i)), BITCOINCASH_BINDIR, BITCOINCASHD))
logging.info('BCH: Started %s %d', BITCOINCASHD, cls.bch_daemons[-1].handle.pid)
waitForRPC(make_rpc_func(i, base_rpc_port=BCH_BASE_RPC_PORT), test_delay_event)
@classmethod
def addPIDInfo(cls, sc, i):
sc.setDaemonPID(Coins.BCH, cls.bch_daemons[i].handle.pid)
@classmethod
def prepareExtraCoins(cls):
cls.bch_addr = callnoderpc(0, 'getnewaddress', ['mining_addr'], base_rpc_port=BCH_BASE_RPC_PORT, wallet='wallet.dat')
if not cls.restore_instance:
num_blocks: int = 200
logging.info('Mining %d BitcoinCash blocks to %s', num_blocks, cls.bch_addr)
callnoderpc(0, 'generatetoaddress', [num_blocks, cls.bch_addr], base_rpc_port=BCH_BASE_RPC_PORT, wallet='wallet.dat')
@classmethod
def addCoinSettings(cls, settings, datadir, node_id):
settings['chainclients']['bitcoincash'] = {
'connection_type': 'rpc',
'manage_daemon': False,
'rpcport': BCH_BASE_RPC_PORT + node_id,
'rpcuser': 'test' + str(node_id),
'rpcpassword': 'test_pass' + str(node_id),
'datadir': os.path.join(datadir, 'bch_' + str(node_id)),
'bindir': BITCOINCASH_BINDIR,
'use_segwit': False,
}
@classmethod
def coins_loop(cls):
super(TestBCH, cls).coins_loop()
ci0 = cls.swap_clients[0].ci(cls.test_coin)
try:
if cls.bch_addr is not None:
ci0.rpc_wallet('generatetoaddress', [1, cls.bch_addr])
except Exception as e:
logging.warning('coins_loop generate {}'.format(e))
@classmethod
def tearDownClass(cls):
logging.info('Finalising Bitcoincash Test')
super(TestBCH, cls).tearDownClass()
stopDaemons(cls.bch_daemons)
cls.bch_daemons.clear()
def mineBlock(self, num_blocks=1):
self.callnoderpc('generatetoaddress', [num_blocks, self.bch_addr])
def check_softfork_active(self, feature_name):
return True
def test_001_nested_segwit(self):
logging.info('---------- Test {} p2sh nested segwit'.format(self.test_coin.name))
logging.info('Skipped')
def test_002_native_segwit(self):
logging.info('---------- Test {} p2sh native segwit'.format(self.test_coin.name))
logging.info('Skipped')
def test_003_cltv(self):
logging.info('---------- Test {} cltv'.format(self.test_coin.name))
ci = self.swap_clients[0].ci(self.test_coin)
self.check_softfork_active('bip65')
chain_height = self.callnoderpc('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 = ci.rpc_wallet('fundrawtransaction', [tx_hex])
utxo_pos = 0 if tx_funded['changepos'] == 1 else 1
tx_signed = ci.rpc_wallet('signrawtransactionwithwallet', [tx_funded['hex'], ])['hex']
txid = ci.rpc('sendrawtransaction', [tx_signed, ])
addr_out = ci.rpc_wallet('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), scriptSig=CScript([script,])))
tx_spend.vout.append(ci.txoType()(ci.make_int(1.0999), script_out))
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 = ci.rpc('sendrawtransaction', [tx_spend_invalid_hex, ])
except Exception as e:
assert ('Locktime requirement not satisfied' in str(e))
else:
assert False, 'Should fail'
txid = ci.rpc('sendrawtransaction', [tx_spend_hex, ])
self.mineBlock()
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.name))
swap_clients = self.swap_clients
ci = self.swap_clients[0].ci(self.test_coin)
self.check_softfork_active('csv')
script = CScript([3, OP_CHECKSEQUENCEVERIFY, ])
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 = ci.rpc_wallet('fundrawtransaction', [tx_hex])
utxo_pos = 0 if tx_funded['changepos'] == 1 else 1
tx_signed = ci.rpc_wallet('signrawtransactionwithwallet', [tx_funded['hex'], ])['hex']
txid = ci.rpc('sendrawtransaction', [tx_signed, ])
addr_out = ci.rpc_wallet('getnewaddress', ['csv test'])
pkh = ci.decodeAddress(addr_out)
script_out = ci.getScriptForPubkeyHash(pkh)
# Double check output type
prev_tx = ci.rpc('decoderawtransaction', [tx_signed, ])
assert (prev_tx['vout'][utxo_pos]['scriptPubKey']['type'] == 'scripthash')
tx_spend = CTransaction()
tx_spend.nVersion = ci.txVersion()
tx_spend.vin.append(CTxIn(COutPoint(int(txid, 16), utxo_pos),
nSequence=3,
scriptSig=CScript([script,])))
tx_spend.vout.append(ci.txoType()(ci.make_int(1.0999), script_out))
tx_spend_hex = ToHex(tx_spend)
try:
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 = ci.rpc('sendrawtransaction', [tx_spend_hex, ])
self.mineBlock(1)
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_005_watchonly(self):
logging.info('---------- Test {} watchonly'.format(self.test_coin.name))
ci = self.swap_clients[0].ci(self.test_coin)
ci1 = self.swap_clients[1].ci(self.test_coin)
addr = ci.rpc_wallet('getnewaddress', ['watchonly test'])
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)
def test_006_getblock_verbosity(self):
super().test_006_getblock_verbosity()
def test_007_hdwallet(self):
logging.info('---------- Test {} hdwallet'.format(self.test_coin.name))
test_seed = '8e54a313e6df8918df6d758fafdbf127a115175fdd2238d0e908dd8093c9ac3b'
test_wif = self.swap_clients[0].ci(self.test_coin).encodeKey(bytes.fromhex(test_seed))
new_wallet_name = random.randbytes(10).hex()
self.callnoderpc('createwallet', [new_wallet_name])
self.callnoderpc('sethdseed', [True, test_wif], wallet=new_wallet_name)
addr = self.callnoderpc('getnewaddress', wallet=new_wallet_name)
self.callnoderpc('unloadwallet', [new_wallet_name])
assert (addr == 'bchreg:qqxr67wf5ltty5jvm44zryywmpt7ntdaa50carjt59')
def test_008_gettxout(self):
super().test_008_gettxout()
def test_009_scantxoutset(self):
super().test_009_scantxoutset()
def test_010_txn_size(self):
logging.info('---------- Test {} txn_size'.format(Coins.BCH))
swap_clients = self.swap_clients
ci = swap_clients[0].ci(Coins.BCH)
pi = swap_clients[0].pi(SwapTypes.XMR_SWAP)
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('listunspent')
# fee_rate is in sats/B
fee_rate: int = 1
a = ci.getNewSecretKey()
b = ci.getNewSecretKey()
A = ci.getPubkey(a)
B = ci.getPubkey(b)
mining_fee = 1000
timelock = 2
b_receive = ci.getNewAddress()
a_refund = ci.getNewAddress()
refundExtraArgs = dict()
lockExtraArgs = dict()
refundExtraArgs['mining_fee'] = 1000
refundExtraArgs['out_1'] = ci.addressToLockingBytecode(a_refund)
refundExtraArgs['out_2'] = ci.addressToLockingBytecode(b_receive)
refundExtraArgs['public_key'] = B
refundExtraArgs['timelock'] = 5
refund_lock_tx_script = pi.genScriptLockTxScript(ci, A, B, **refundExtraArgs)
# will make use of this in `createSCLockRefundTx`
refundExtraArgs['refund_lock_tx_script'] = refund_lock_tx_script
# lock script
lockExtraArgs['mining_fee'] = 1000
lockExtraArgs['out_1'] = ci.addressToLockingBytecode(b_receive)
lockExtraArgs['out_2'] = ci.scriptToP2SH32LockingBytecode(refund_lock_tx_script)
lockExtraArgs['public_key'] = A
lockExtraArgs['timelock'] = 2
lock_tx_script = pi.genScriptLockTxScript(ci, A, B, **lockExtraArgs)
lock_tx = ci.createSCLockTx(amount, lock_tx_script)
lock_tx = ci.fundSCLockTx(lock_tx, fee_rate)
lock_tx = ci.signTxWithWallet(lock_tx)
print(lock_tx.hex())
unspents_after = ci.rpc('listunspent')
assert (len(unspents) > len(unspents_after))
tx_decoded = ci.rpc('decoderawtransaction', [lock_tx.hex()])
txid = tx_decoded['txid']
vsize = tx_decoded['size']
expect_fee_int = round(fee_rate * vsize)
expect_fee = ci.format_amount(expect_fee_int)
out_value: int = 0
for txo in tx_decoded['vout']:
if 'value' in txo:
out_value += ci.make_int(txo['value'])
in_value: int = 0
for txi in tx_decoded['vin']:
for utxo in unspents:
if 'vout' not in utxo:
continue
if utxo['txid'] == txi['txid'] and utxo['vout'] == txi['vout']:
in_value += ci.make_int(utxo['amount'])
break
fee_value = in_value - out_value
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)
assert (wallet_tx_fee == expect_fee_int)
pkh_out = ci.decodeAddress(b_receive)
msg = sha256(ci.addressToLockingBytecode(b_receive))
# leader creates an adaptor signature for follower and transmits it to the follower
aAdaptorSig = ecdsaotves_enc_sign(a, B, msg)
# alice verifies the adaptor signature
assert (ecdsaotves_enc_verify(A, B, msg, aAdaptorSig))
# alice decrypts the adaptor signature
aAdaptorSig_dec = ecdsaotves_dec_sig(b, aAdaptorSig)
fee_info = {}
lock_spend_tx = ci.createSCLockSpendTx(lock_tx, lock_tx_script, pkh_out, mining_fee, fee_info=fee_info, ves=aAdaptorSig_dec)
vsize_estimated: int = fee_info['vsize']
tx_decoded = ci.rpc('decoderawtransaction', [lock_spend_tx.hex()])
print('lock_spend_tx', lock_spend_tx.hex(), '\n', 'tx_decoded', tx_decoded)
txid = tx_decoded['txid']
tx_decoded = ci.rpc('decoderawtransaction', [lock_spend_tx.hex()])
vsize_actual: int = tx_decoded['size']
assert (vsize_actual <= vsize_estimated and vsize_estimated - vsize_actual < 4)
assert (ci.rpc('sendrawtransaction', [lock_spend_tx.hex()]) == txid)
expect_size: int = ci.xmr_swap_a_lock_spend_tx_vsize()
assert (expect_size >= vsize_actual)
assert (expect_size - vsize_actual < 10)
def test_011_p2sh(self):
# Not used in bsx for native-segwit coins
logging.info('---------- Test {} p2sh'.format(self.test_coin.name))
swap_clients = self.swap_clients
ci = self.swap_clients[0].ci(self.test_coin)
script = CScript([2, 2, OP_EQUAL, ])
script_dest = ci.get_p2sh_script_pubkey(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 = ci.rpc_wallet('fundrawtransaction', [tx_hex])
utxo_pos = 0 if tx_funded['changepos'] == 1 else 1
tx_signed = ci.rpc_wallet('signrawtransactionwithwallet', [tx_funded['hex'], ])['hex']
txid = ci.rpc('sendrawtransaction', [tx_signed, ])
addr_out = ci.rpc_wallet('getnewaddress', ['csv test'])
pkh = ci.decodeAddress(addr_out)
script_out = ci.getScriptForPubkeyHash(pkh)
# Double check output type
prev_tx = ci.rpc('decoderawtransaction', [tx_signed, ])
assert (prev_tx['vout'][utxo_pos]['scriptPubKey']['type'] == 'scripthash')
tx_spend = CTransaction()
tx_spend.nVersion = ci.txVersion()
tx_spend.vin.append(CTxIn(COutPoint(int(txid, 16), utxo_pos),
scriptSig=CScript([script,])))
tx_spend.vout.append(ci.txoType()(ci.make_int(1.0999), script_out))
tx_spend_hex = ToHex(tx_spend)
txid = ci.rpc('sendrawtransaction', [tx_spend_hex, ])
self.mineBlock(1)
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_011_p2sh32(self):
# Not used in bsx for native-segwit coins
logging.info('---------- Test {} p2sh32'.format(self.test_coin.name))
swap_clients = self.swap_clients
ci = self.swap_clients[0].ci(self.test_coin)
script = CScript([2, 2, OP_EQUAL, ])
script_dest = ci.scriptToP2SH32LockingBytecode(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 = ci.rpc_wallet('fundrawtransaction', [tx_hex])
utxo_pos = 0 if tx_funded['changepos'] == 1 else 1
tx_signed = ci.rpc_wallet('signrawtransactionwithwallet', [tx_funded['hex'], ])['hex']
txid = ci.rpc('sendrawtransaction', [tx_signed, ])
addr_out = ci.rpc_wallet('getnewaddress', ['csv test'])
pkh = ci.decodeAddress(addr_out)
script_out = ci.getScriptForPubkeyHash(pkh)
# Double check output type
prev_tx = ci.rpc('decoderawtransaction', [tx_signed, ])
assert (prev_tx['vout'][utxo_pos]['scriptPubKey']['type'] == 'scripthash')
tx_spend = CTransaction()
tx_spend.nVersion = ci.txVersion()
tx_spend.vin.append(CTxIn(COutPoint(int(txid, 16), utxo_pos),
scriptSig=CScript([script,])))
tx_spend.vout.append(ci.txoType()(ci.make_int(1.0999), script_out))
tx_spend_hex = ToHex(tx_spend)
txid = ci.rpc('sendrawtransaction', [tx_spend_hex, ])
self.mineBlock(1)
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_012_p2sh_p2wsh(self):
logging.info('---------- Test {} p2sh-p2wsh'.format(self.test_coin.name))
logging.info('Skipped')
def test_01_a_full_swap(self):
super().test_01_a_full_swap()
def test_01_b_full_swap_reverse(self):
self.prepare_balance(Coins.BCH, 100.0, 1801, 1800)
super().test_01_b_full_swap_reverse()
def test_01_c_full_swap_to_part(self):
super().test_01_c_full_swap_to_part()
def test_01_d_full_swap_from_part(self):
self.prepare_balance(Coins.BCH, 100.0, 1801, 1800)
super().test_01_d_full_swap_from_part()
def test_02_a_leader_recover_a_lock_tx(self):
super().test_02_a_leader_recover_a_lock_tx()
def test_03_a_follower_recover_a_lock_tx(self):
self.do_test_03_follower_recover_a_lock_tx(self.test_coin_from, Coins.XMR, with_mercy=True)
def test_03_b_follower_recover_a_lock_tx_reverse(self):
self.prepare_balance(Coins.BCH, 100.0, 1801, 1800)
self.prepare_balance(Coins.XMR, 100.0, 1800, 1801)
self.do_test_03_follower_recover_a_lock_tx(Coins.XMR, self.test_coin_from, lock_value=12, with_mercy=True)
def test_03_c_follower_recover_a_lock_tx_to_part(self):
super().test_03_c_follower_recover_a_lock_tx_to_part()
def test_03_d_follower_recover_a_lock_tx_from_part(self):
self.prepare_balance(Coins.BCH, 100.0, 1801, 1800)
super().test_03_d_follower_recover_a_lock_tx_from_part()
def test_04_a_follower_recover_b_lock_tx(self):
super().test_04_a_follower_recover_b_lock_tx()
def test_04_b_follower_recover_b_lock_tx_reverse(self):
self.prepare_balance(Coins.BCH, 100.0, 1801, 1800)
super().test_04_b_follower_recover_b_lock_tx_reverse()
def test_04_c_follower_recover_b_lock_tx_to_part(self):
super().test_04_c_follower_recover_b_lock_tx_to_part()
def test_04_d_follower_recover_b_lock_tx_from_part(self):
self.prepare_balance(Coins.BCH, 100.0, 1801, 1800)
super().test_04_d_follower_recover_b_lock_tx_from_part()
def test_05_self_bid(self):
self.prepare_balance(Coins.BCH, 100.0, 1801, 1800)
super().test_05_self_bid()
def test_05_self_bid_to_part(self):
self.prepare_balance(Coins.BCH, 100.0, 1801, 1800)
super().test_05_self_bid_to_part()
def test_05_self_bid_from_part(self):
self.prepare_balance(Coins.BCH, 100.0, 1801, 1800)
super().test_05_self_bid_from_part()
def test_05_self_bid_rev(self):
self.prepare_balance(Coins.BCH, 100.0, 1801, 1800)
super().test_05_self_bid_rev()
def test_06_preselect_inputs(self):
tla_from = self.test_coin.name
logging.info('---------- Test {} Preselected inputs'.format(tla_from))
logging.info('Skipped')
def test_07_expire_stuck_accepted(self):
super().test_07_expire_stuck_accepted()
def test_08_insufficient_funds(self):
super().test_08_insufficient_funds()
def test_08_insufficient_funds_rev(self):
self.prepare_balance(Coins.BCH, 100.0, 1801, 1800)
super().test_08_insufficient_funds_rev()

View File

@@ -105,7 +105,7 @@ class TestFunctions(BaseTest):
id_bidder: int = self.node_b_id
swap_clients = self.swap_clients
reverse_bid: bool = coin_from in swap_clients[id_offerer].scriptless_coins
reverse_bid: bool = swap_clients[0].is_reverse_ads_bid(coin_from, coin_to)
ci_from = swap_clients[id_offerer].ci(coin_from)
ci_to = swap_clients[id_bidder].ci(coin_to)
ci_part0 = swap_clients[id_offerer].ci(Coins.PART)
@@ -229,7 +229,7 @@ class TestFunctions(BaseTest):
id_bidder: int = self.node_b_id
swap_clients = self.swap_clients
reverse_bid: bool = coin_from in swap_clients[id_offerer].scriptless_coins
reverse_bid: bool = swap_clients[0].is_reverse_ads_bid(coin_from, coin_to)
ci_from = swap_clients[id_offerer].ci(coin_from)
ci_to = swap_clients[id_offerer].ci(coin_to)
@@ -264,17 +264,17 @@ class TestFunctions(BaseTest):
# TODO: Discard block rewards
# assert (node0_from_before - node0_from_after < 0.02)
def do_test_03_follower_recover_a_lock_tx(self, coin_from, coin_to, lock_value: int = 32):
logging.info('---------- Test {} to {} follower recovers coin a lock tx'.format(coin_from.name, coin_to.name))
def do_test_03_follower_recover_a_lock_tx(self, coin_from, coin_to, lock_value: int = 32, with_mercy: bool = False):
logging.info('---------- Test {} to {} follower recovers coin a lock tx{}'.format(coin_from.name, coin_to.name, ' (with mercy tx)' if with_mercy else ''))
# Leader is too slow to recover the coin a lock tx and follower swipes it
# coin b lock tx remains unspent
# Coin B lock tx remains unspent unless a mercy output revealing the follower's keyshare is sent
id_offerer: int = self.node_a_id
id_bidder: int = self.node_b_id
swap_clients = self.swap_clients
reverse_bid: bool = coin_from in swap_clients[id_offerer].scriptless_coins
reverse_bid: bool = swap_clients[0].is_reverse_ads_bid(coin_from, coin_to)
ci_from = swap_clients[id_offerer].ci(coin_from)
ci_to = swap_clients[id_offerer].ci(coin_to)
@@ -282,6 +282,8 @@ class TestFunctions(BaseTest):
id_follower: int = id_offerer if reverse_bid else id_bidder
logging.info(f'Offerer, bidder, leader, follower: {id_offerer}, {id_bidder}, {id_leader}, {id_follower}')
swap_clients[id_follower].ci(coin_from if reverse_bid else coin_to)._altruistic = with_mercy
js_w0_before = read_json_api(1800 + id_offerer, 'wallets')
js_w1_before = read_json_api(1800 + id_bidder, 'wallets')
@@ -296,13 +298,20 @@ class TestFunctions(BaseTest):
bid_id = swap_clients[id_bidder].postXmrBid(offer_id, offer.amount_from)
wait_for_bid(test_delay_event, swap_clients[id_offerer], bid_id, BidStates.BID_RECEIVED)
swap_clients[id_follower].setBidDebugInd(bid_id, DebugTypes.BID_STOP_AFTER_COIN_A_LOCK)
swap_clients[id_leader].setBidDebugInd(bid_id, DebugTypes.BID_DONT_SPEND_COIN_A_LOCK_REFUND)
debug_type = DebugTypes.BID_DONT_SPEND_COIN_A_LOCK_REFUND2 if with_mercy else DebugTypes.BID_DONT_SPEND_COIN_A_LOCK_REFUND
swap_clients[id_leader].setBidDebugInd(bid_id, debug_type)
debug_type = DebugTypes.BID_DONT_SPEND_COIN_B_LOCK if with_mercy else DebugTypes.BID_STOP_AFTER_COIN_A_LOCK
swap_clients[id_follower].setBidDebugInd(bid_id, debug_type)
swap_clients[id_leader].setBidDebugInd(bid_id, DebugTypes.WAIT_FOR_COIN_B_LOCK_BEFORE_REFUND, False)
swap_clients[id_follower].setBidDebugInd(bid_id, DebugTypes.WAIT_FOR_COIN_B_LOCK_BEFORE_REFUND, False)
swap_clients[id_offerer].acceptBid(bid_id)
leader_sent_bid: bool = True if reverse_bid else False
wait_for_bid(test_delay_event, swap_clients[id_leader], bid_id, BidStates.BID_STALLED_FOR_TEST, wait_for=(self.extra_wait_time + 180), sent=leader_sent_bid)
expect_state = (BidStates.XMR_SWAP_NOSCRIPT_TX_REDEEMED, BidStates.SWAP_COMPLETED) if with_mercy else BidStates.BID_STALLED_FOR_TEST
wait_for_bid(test_delay_event, swap_clients[id_leader], bid_id, expect_state, wait_for=(self.extra_wait_time + 180), sent=leader_sent_bid)
wait_for_bid(test_delay_event, swap_clients[id_follower], bid_id, BidStates.XMR_SWAP_FAILED_SWIPED, wait_for=(self.extra_wait_time + 80), sent=(not leader_sent_bid))
js_w1_after = read_json_api(1800 + id_bidder, 'wallets')
@@ -325,7 +334,7 @@ class TestFunctions(BaseTest):
id_bidder: int = self.node_b_id
swap_clients = self.swap_clients
reverse_bid: bool = coin_from in swap_clients[id_offerer].scriptless_coins
reverse_bid: bool = swap_clients[0].is_reverse_ads_bid(coin_from, coin_to)
ci_from = swap_clients[id_offerer].ci(coin_from)
ci_to = swap_clients[id_offerer].ci(coin_to)
@@ -340,6 +349,7 @@ class TestFunctions(BaseTest):
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)
logging.info(f'amount from, rate, amount to: {amt_swap}, {rate_swap}, {amt_swap * rate_swap}')
offer_id = swap_clients[id_offerer].postOffer(
coin_from, coin_to, amt_swap, rate_swap, amt_swap, SwapTypes.XMR_SWAP,
lock_type=TxLockTypes.SEQUENCE_LOCK_BLOCKS, lock_value=lock_value)
@@ -913,6 +923,7 @@ class BasicSwapTest(TestFunctions):
self.do_test_02_leader_recover_a_lock_tx(self.test_coin_from, Coins.PART)
def test_02_leader_recover_a_lock_tx_from_part(self):
self.prepare_balance(self.test_coin_from, 100.0, 1801, 1800)
self.do_test_02_leader_recover_a_lock_tx(Coins.PART, self.test_coin_from)
def test_03_a_follower_recover_a_lock_tx(self):
@@ -934,6 +945,18 @@ class BasicSwapTest(TestFunctions):
def test_03_d_follower_recover_a_lock_tx_from_part(self):
self.do_test_03_follower_recover_a_lock_tx(Coins.PART, self.test_coin_from)
def test_03_e_follower_recover_a_lock_tx_mercy_release(self):
if not self.has_segwit:
return
self.do_test_03_follower_recover_a_lock_tx(self.test_coin_from, Coins.XMR, with_mercy=True)
def test_03_f_follower_recover_a_lock_tx_mercy_release_reverse(self):
if not self.has_segwit:
return
self.prepare_balance(Coins.XMR, 100.0, 1800, 1801)
self.prepare_balance(self.test_coin_from, 100.0, 1801, 1800)
self.do_test_03_follower_recover_a_lock_tx(Coins.XMR, self.test_coin_from, with_mercy=True)
def test_04_a_follower_recover_b_lock_tx(self):
if not self.has_segwit:
return

View File

@@ -1031,6 +1031,8 @@ class Test(BaseTest):
logging.info('---------- Test PART to XMR follower recovers coin a lock tx')
swap_clients = self.swap_clients
swap_clients[1].ci(Coins.PART)._altruistic = False
offer_id = swap_clients[0].postOffer(
Coins.PART, Coins.XMR, 101 * COIN, 0.13 * XMR_COIN, 101 * COIN, SwapTypes.XMR_SWAP,
lock_type=TxLockTypes.SEQUENCE_LOCK_BLOCKS, lock_value=16)
@@ -1062,6 +1064,39 @@ class Test(BaseTest):
bidder_states = [s for s in bidder_states if s[1] != 'Bid Stalled (debug)']
assert (compare_bid_states(bidder_states, self.states_bidder[2]) is True)
def test_03b_follower_recover_a_lock_tx_with_mercy(self):
logging.info('---------- Test PART to XMR follower recovers coin a lock tx with mercy output')
swap_clients = self.swap_clients
swap_clients[1].ci(Coins.PART)._altruistic = True
offer_id = swap_clients[0].postOffer(
Coins.PART, Coins.XMR, 101 * COIN, 0.13 * XMR_COIN, 101 * COIN, SwapTypes.XMR_SWAP,
lock_type=TxLockTypes.SEQUENCE_LOCK_BLOCKS, lock_value=16)
wait_for_offer(test_delay_event, swap_clients[1], offer_id)
offer = swap_clients[1].getOffer(offer_id)
bid_id = swap_clients[1].postXmrBid(offer_id, offer.amount_from)
wait_for_bid(test_delay_event, swap_clients[0], bid_id, BidStates.BID_RECEIVED)
bid, xmr_swap = swap_clients[0].getXmrBid(bid_id)
assert (xmr_swap)
swap_clients[1].setBidDebugInd(bid_id, DebugTypes.BID_DONT_SPEND_COIN_B_LOCK)
swap_clients[0].setBidDebugInd(bid_id, DebugTypes.BID_DONT_SPEND_COIN_A_LOCK_REFUND2)
swap_clients[0].setBidDebugInd(bid_id, DebugTypes.WAIT_FOR_COIN_B_LOCK_BEFORE_REFUND, False)
swap_clients[1].setBidDebugInd(bid_id, DebugTypes.WAIT_FOR_COIN_B_LOCK_BEFORE_REFUND, False)
swap_clients[0].acceptXmrBid(bid_id)
wait_for_bid(test_delay_event, swap_clients[0], bid_id, (BidStates.XMR_SWAP_NOSCRIPT_TX_REDEEMED, BidStates.SWAP_COMPLETED), wait_for=220)
wait_for_bid(test_delay_event, swap_clients[1], bid_id, BidStates.XMR_SWAP_FAILED_SWIPED, wait_for=120, sent=True)
wait_for_none_active(test_delay_event, 1800)
wait_for_none_active(test_delay_event, 1801)
def test_04_follower_recover_b_lock_tx(self):
logging.info('---------- Test PART to XMR follower recovers coin b lock tx')