Add bitcoincash support for prepare and run scripts, add bitcoincash to testing suite, groundwork for bch-xmr atomic swap protocol

This commit is contained in:
mainnet-pat
2024-10-07 16:48:24 +00:00
committed by tecnovert
parent 745d1460e5
commit 1b43806d51
20 changed files with 937 additions and 9 deletions

View File

@@ -251,6 +251,7 @@ class BasicSwap(BaseApp):
protocolInterfaces = {
SwapTypes.SELLER_FIRST: atomic_swap_1.AtomicSwapInterface(),
SwapTypes.XMR_SWAP: xmr_swap_1.XmrSwapInterface(),
SwapTypes.XMR_BCH_SWAP: xmr_swap_1.XmrBchSwapInterface(),
}
def __init__(self, fp, data_dir, settings, chain, log_name='BasicSwap', transient_instance=False):
@@ -688,6 +689,9 @@ class BasicSwap(BaseApp):
elif coin == Coins.BTC:
from .interface.btc import BTCInterface
return BTCInterface(self.coin_clients[coin], self.chain, self)
elif coin == Coins.BCH:
from .interface.bch import BCHInterface
return BCHInterface(self.coin_clients[coin], self.chain, self)
elif coin == Coins.LTC:
from .interface.ltc import LTCInterface, LTCInterfaceMWEB
interface = LTCInterface(self.coin_clients[coin], self.chain, self)

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):

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

@@ -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('BITCOIN_VERSION', '27.1.0')
BITCOINCASH_VERSION_TAG = os.getenv('BITCOIN_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())
@@ -186,6 +191,12 @@ 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', 8334))
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')
@@ -513,8 +524,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 +713,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 +760,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 +820,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 +861,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)
@@ -1032,6 +1055,10 @@ 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=2000\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':
@@ -1182,6 +1209,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':
@@ -1353,7 +1382,7 @@ def initialise_wallets(particl_wallet_mnemonic, with_coins, data_dir, settings,
pass
else:
if coin_settings['manage_daemon']:
filename = coin_name + 'd' + ('.exe' if os.name == 'nt' else '')
filename = (coin_name if not coin_name == "bitcoincash" else "bitcoin") + 'd' + ('.exe' if os.name == 'nt' else '')
coin_args = ['-nofindpeers', '-nostaking'] if c == Coins.PART else []
if c == Coins.FIRO:
@@ -1755,6 +1784,19 @@ def main():
'conf_target': 2,
'core_version_group': 22,
},
'bitcoincash': {
'connection_type': 'rpc' if 'bitcoincash' in with_coins else 'none',
'manage_daemon': True if ('bitcoincash' in with_coins and BCH_RPC_HOST == '127.0.0.1') else False,
'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'),
'use_segwit': False,
'blocks_confirmed': 1,
'conf_target': 2,
'core_version_group': 22,
},
'litecoin': {
'connection_type': 'rpc',
'manage_daemon': shouldManageDaemon('LTC'),
@@ -1917,6 +1959,9 @@ def main():
if BTC_RPC_USER != '':
chainclients['bitcoin']['rpcuser'] = BTC_RPC_USER
chainclients['bitcoin']['rpcpassword'] = BTC_RPC_PWD
if BCH_RPC_USER != '':
chainclients['bitcoin']['rpcuser'] = BCH_RPC_USER
chainclients['bitcoin']['rpcpassword'] = BCH_RPC_PWD
if XMR_RPC_USER != '':
chainclients['monero']['rpcuser'] = XMR_RPC_USER
chainclients['monero']['rpcpassword'] = XMR_RPC_PWD

View File

@@ -214,7 +214,7 @@ 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 = (c if not c == "bitcoincash" else "bitcoin") + 'd' + ('.exe' if os.name == 'nt' else '')
daemons.append(startXmrDaemon(v['datadir'], v['bindir'], filename))
pid = daemons[-1].handle.pid
swap_client.log.info('Started {} {}'.format(filename, pid))
@@ -280,7 +280,7 @@ 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 '')
filename = (c if not c == "bitcoincash" else "bitcoin") + 'd' + ('.exe' if os.name == 'nt' else '')
daemons.append(startDaemon(v['datadir'], v['bindir'], filename))
pid = daemons[-1].handle.pid
pids.append((c, pid))

View File

@@ -30,6 +30,7 @@ class Coins(IntEnum):
NAV = 14
LTC_MWEB = 15
# ZANO = 16
BCH = 17
chainparams = {
@@ -432,7 +433,45 @@ chainparams = {
'min_amount': 1000,
'max_amount': 100000 * COIN,
}
}
},
Coins.BCH: {
'name': 'bitcoincash',
'ticker': 'BCH',
'message_magic': 'Bitcoin Signed Message:\n',
'blocks_target': 60 * 2,
'decimal_places': 8,
'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

@@ -36,3 +36,8 @@ 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)
BITCOINCASH_BINDIR = os.path.expanduser(os.getenv('BITCOINCASH_BINDIR', os.path.join(DEFAULT_TEST_BINDIR, 'bitcoincash')))
BITCOINCASHD = os.getenv('BITCOINCASHD', 'bitcoind' + bin_suffix)
BITCOINCASH_CLI = os.getenv('BITCOINCASH_CLI', 'bitcoin-cli' + bin_suffix)
BITCOINCASH_TX = os.getenv('BITCOINCASH_TX', 'bitcoin-tx' + bin_suffix)

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

@@ -0,0 +1,119 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (c) 2020-2023 tecnovert
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
from typing import Union
from basicswap.contrib.test_framework.messages import COutPoint, CTransaction, CTxIn, CTxOut
from basicswap.util import ensure, i2h
from .btc import BTCInterface, findOutput
from basicswap.rpc import make_rpc_func
from basicswap.chainparams import Coins, chainparams
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_EQUAL, OP_EQUALVERIFY, OP_HASH256, OP_DUP, OP_HASH160, OP_CHECKSIG
from basicswap.contrib.test_framework.script import (
CScript, CScriptOp,
)
class BCHInterface(BTCInterface):
@staticmethod
def coin_type():
return Coins.BCH
def __init__(self, coin_settings, network, swap_client=None):
super(BCHInterface, self).__init__(coin_settings, network, swap_client)
def decodeAddress(self, address: str) -> bytes:
return bytes(Address.from_string(address).payload)
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)
prefix = self.chainparams_network()['hrp']
address = Address("P2PKH", b'\x76\xa9\x14' + pkh + b'\x88\xac')
address.prefix = prefix
return address.cash_address()
def getNewAddress(self, use_segwit: bool = False, label: str = 'swap_receive') -> str:
args = [label]
return self.rpc_wallet('getnewaddress', args)
def addressToLockingBytecode(self, address: str) -> bytes:
return b'\x76\xa9\x14' + bytes(Address.from_string(address).payload) + b'\x88\xac'
def getScriptDest(self, script):
return self.scriptToP2SH32LockingBytecode(script)
def scriptToP2SH32LockingBytecode(self, script: Union[bytes, str]) -> bytes:
if isinstance(script, str):
script = bytes.fromhex(script)
return CScript([
CScriptOp(OP_HASH256),
sha256(sha256(script)),
CScriptOp(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 getScriptForPubkeyHash(self, pkh: bytes) -> CScript:
return CScript([
CScriptOp(OP_DUP),
CScriptOp(OP_HASH160),
pkh,
CScriptOp(OP_EQUALVERIFY),
CScriptOp(OP_CHECKSIG),
])
def getTxSize(self, tx: CTransaction) -> int:
return len(tx.serialize_without_witness())
def getScriptScriptSig(self, script: bytes, ves: bytes) -> 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, ves=None, fee_info={}):
# tx_fee_rate in this context is equal to `mining_fee` contract param
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
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()

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

View File

@@ -6,6 +6,34 @@
import traceback
import unittest
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,
)
from basicswap.util import (
ensure,
)
@@ -193,3 +221,82 @@ class XmrSwapInterface(ProtocolInterface):
ctx.nLockTime = 0
return ctx.serialize()
class XmrBchSwapInterface(ProtocolInterface):
swap_type = SwapTypes.XMR_BCH_SWAP
def genScriptLockTxScript(self, mining_fee: int, out_1: bytes, out_2: bytes, public_key: bytes, timelock: int) -> CScript:
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
CScriptOp(OP_TXINPUTCOUNT),
CScriptOp(OP_1), CScriptOp(OP_NUMEQUALVERIFY),
CScriptOp(OP_TXOUTPUTCOUNT),
CScriptOp(OP_1), CScriptOp(OP_NUMEQUALVERIFY),
# // int miningFee
mining_fee,
# // Verify pre-agreed mining fee and that the rest of BCH is forwarded
# // to the output.
CScriptOp(OP_0), CScriptOp(OP_UTXOVALUE),
CScriptOp(OP_0), CScriptOp(OP_OUTPUTVALUE),
CScriptOp(OP_SUB), CScriptOp(OP_NUMEQUALVERIFY),
# # // Verify that any CashTokens are forwarded to the output.
CScriptOp(OP_0), CScriptOp(OP_UTXOTOKENCATEGORY),
CScriptOp(OP_0), CScriptOp(OP_OUTPUTTOKENCATEGORY),
CScriptOp(OP_EQUALVERIFY),
CScriptOp(OP_0), CScriptOp(OP_UTXOTOKENCOMMITMENT),
CScriptOp(OP_0), CScriptOp(OP_OUTPUTTOKENCOMMITMENT),
CScriptOp(OP_EQUALVERIFY),
CScriptOp(OP_0), CScriptOp(OP_UTXOTOKENAMOUNT),
CScriptOp(OP_0), CScriptOp(OP_OUTPUTTOKENAMOUNT),
CScriptOp(OP_NUMEQUALVERIFY),
# // If sequence is not used then it is a regular swap TX.
CScriptOp(OP_0), CScriptOp(OP_INPUTSEQUENCENUMBER),
CScriptOp(OP_NOTIF),
# // bytes aliceOutput
out_1,
# // Verify that the BCH and/or CashTokens are forwarded to Alice's
# // output.
CScriptOp(OP_0), CScriptOp(OP_OUTPUTBYTECODE),
CScriptOp(OP_OVER), CScriptOp(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.
CScriptOp(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 witheld the encrypted signature.
CScriptOp(OP_ELSE),
# // int timelock_0
timelock,
# // Verify refund timelock.
CScriptOp(OP_CHECKSEQUENCEVERIFY), CScriptOp(OP_DROP),
# // bytes refundLockingBytecode
out_2,
# // Verify that the BCH and/or CashTokens are forwarded to Refund
# // contract.
CScriptOp(OP_0), CScriptOp(OP_OUTPUTBYTECODE),
CScriptOp(OP_EQUAL),
# // BCH and/or CashTokens are simply forwarded to Refund contract.
CScriptOp(OP_ENDIF)
])