diff --git a/basicswap/basicswap.py b/basicswap/basicswap.py index d2c9e0a..e99b431 100644 --- a/basicswap/basicswap.py +++ b/basicswap/basicswap.py @@ -34,6 +34,7 @@ from .interface.part import PARTInterface, PARTInterfaceAnon, PARTInterfaceBlind from . import __version__ from .rpc import escape_rpcauth from .rpc_xmr import make_xmr_rpc2_func +from .rpc_wow import make_wow_rpc2_func from .ui.util import getCoinName, known_chart_coins from .util import ( AutomationConstraint, @@ -180,6 +181,21 @@ def threadPollXMRChainState(swap_client, coin_type): swap_client.chainstate_delay_event.wait(random.randrange(20, 30)) # Random to stagger updates +def threadPollWOWChainState(swap_client, coin_type): + ci = swap_client.ci(coin_type) + cc = swap_client.coin_clients[coin_type] + while not swap_client.chainstate_delay_event.is_set(): + try: + new_height = ci.getChainHeight() + if new_height != cc['chain_height']: + swap_client.log.debug('New {} block at height: {}'.format(ci.ticker(), new_height)) + with swap_client.mxDB: + cc['chain_height'] = new_height + except Exception as e: + swap_client.log.warning('threadPollWOWChainState {}, error: {}'.format(ci.ticker(), str(e))) + swap_client.chainstate_delay_event.wait(random.randrange(20, 30)) # Random to stagger updates + + def threadPollChainState(swap_client, coin_type): ci = swap_client.ci(coin_type) cc = swap_client.coin_clients[coin_type] @@ -281,7 +297,7 @@ class BasicSwap(BaseApp): # TODO: Set dynamically self.balance_only_coins = (Coins.LTC_MWEB, ) - self.scriptless_coins = (Coins.XMR, Coins.PART_ANON, Coins.FIRO) + self.scriptless_coins = (Coins.XMR, Coins.WOW, Coins.PART_ANON, Coins.FIRO) self.adaptor_swap_only_coins = self.scriptless_coins + (Coins.PART_BLIND, ) self.coins_without_segwit = (Coins.PIVX, Coins.DASH, Coins.NMC) @@ -523,7 +539,7 @@ class BasicSwap(BaseApp): if self.coin_clients[coin]['connection_type'] == 'rpc': if coin == Coins.DCR: self.coin_clients[coin]['walletrpcport'] = chain_client_settings['walletrpcport'] - elif coin == Coins.XMR: + elif coin in (Coins.XMR, Coins.WOW): self.coin_clients[coin]['rpctimeout'] = chain_client_settings.get('rpctimeout', 60) self.coin_clients[coin]['walletrpctimeout'] = chain_client_settings.get('walletrpctimeout', 120) self.coin_clients[coin]['walletrpctimeoutlong'] = chain_client_settings.get('walletrpctimeoutlong', 600) @@ -561,9 +577,9 @@ class BasicSwap(BaseApp): if self.use_tor_proxy: have_cc_tor_opt = 'use_tor' in chain_client_settings if have_cc_tor_opt and chain_client_settings['use_tor'] is False: - self.log.warning('use_tor is true for system but false for XMR.') + self.log.warning('use_tor is true for system but false for ' + coin + '.') elif have_cc_tor_opt is False and is_private_ip_address(node_host): - self.log.warning(f'Not using proxy for XMR node at private ip address {node_host}.') + self.log.warning(f'Not using proxy for {coin} node at private ip address {node_host}.') else: proxy_host = self.tor_proxy_host proxy_port = self.tor_proxy_port @@ -585,7 +601,10 @@ class BasicSwap(BaseApp): if proxy_host: self.log.info(f'Connecting through proxy at {proxy_host}.') - return make_xmr_rpc2_func(rpcport, daemon_login, rpchost, proxy_host=proxy_host, proxy_port=proxy_port) + if coin == Coins.XMR: + return make_xmr_rpc2_func(rpcport, daemon_login, rpchost, proxy_host=proxy_host, proxy_port=proxy_port) + if coin == Coins.WOW: + return make_wow_rpc2_func(rpcport, daemon_login, rpchost, proxy_host=proxy_host, proxy_port=proxy_port) daemon_login = None if coin_settings.get('rpcuser', '') != '': @@ -687,6 +706,12 @@ class BasicSwap(BaseApp): chain_client_settings = self.getChainClientSettings(coin) xmr_i.setWalletFilename(chain_client_settings['walletfile']) return xmr_i + elif coin == Coins.WOW: + from .interface.wow import WOWInterface + wow_i = WOWInterface(self.coin_clients[coin], self.chain, self) + chain_client_settings = self.getChainClientSettings(coin) + wow_i.setWalletFilename(chain_client_settings['walletfile']) + return wow_i elif coin == Coins.PIVX: from .interface.pivx import PIVXInterface return PIVXInterface(self.coin_clients[coin], self.chain, self) @@ -711,7 +736,7 @@ class BasicSwap(BaseApp): def setCoinRunParams(self, coin): cc = self.coin_clients[coin] - if coin == Coins.XMR: + if coin in (Coins.XMR, Coins.WOW): return if cc['connection_type'] == 'rpc' and cc['rpcauth'] is None: chain_client_settings = self.getChainClientSettings(coin) @@ -782,8 +807,14 @@ class BasicSwap(BaseApp): core_version = ci.getDaemonVersion() self.log.info('%s Core version %d', ci.coin_name(), core_version) self.coin_clients[c]['core_version'] = core_version + # thread_func = threadPollXMRChainState if c in (Coins.XMR, Coins.WOW) else threadPollChainState + if c == Coins.XMR: + thread_func = threadPollXMRChainState + elif c == Coins.WOW: + thread_func = threadPollWOWChainState + else: + thread_func = threadPollChainState - thread_func = threadPollXMRChainState if c == Coins.XMR else threadPollChainState t = threading.Thread(target=thread_func, args=(self, c)) self.threads.append(t) t.start() @@ -808,6 +839,12 @@ class BasicSwap(BaseApp): except Exception as e: self.log.warning('Can\'t open XMR wallet, could be locked.') continue + elif c == Coins.WOW: + try: + ci.ensureWalletExists() + except Exception as e: + self.log.warning('Can\'t open WOW wallet, could be locked.') + continue elif c == Coins.LTC: ci_mweb = self.ci(Coins.LTC_MWEB) is_encrypted, _ = self.getLockedState() @@ -858,7 +895,7 @@ class BasicSwap(BaseApp): self.log.info('Scanned %d unread messages.', nm) def stopDaemon(self, coin) -> None: - if coin in (Coins.XMR, Coins.DCR): + if coin in (Coins.XMR, Coins.DCR, Coins.WOW): return num_tries = 10 authcookiepath = os.path.join(self.getChainDatadirPath(coin), '.cookie') @@ -897,7 +934,7 @@ class BasicSwap(BaseApp): if with_wallet: self.waitForDaemonRPC(coin_type, with_wallet=False) - if coin_type in (Coins.XMR,): + if coin_type in (Coins.XMR, Coins.WOW): return ci = self.ci(coin_type) # checkWallets can adjust the wallet name. @@ -932,7 +969,7 @@ class BasicSwap(BaseApp): raise ValueError('{} has an unexpected wallet seed and "restrict_unknown_seed_wallets" is enabled.'.format(ci.coin_name())) if self.coin_clients[c]['connection_type'] != 'rpc': continue - if c == Coins.XMR: + if c in (Coins.XMR, Coins.WOW): continue # TODO synced = round(ci.getBlockchainInfo()['verificationprogress'], 3) if synced < 1.0: @@ -1035,7 +1072,7 @@ class BasicSwap(BaseApp): db_key_coin_name = ci.coin_name().lower() self.log.info('Initialising {} wallet.'.format(ci.coin_name())) - if coin_type == Coins.XMR: + if coin_type in (Coins.XMR, Coins.WOW): key_view = self.getWalletKey(coin_type, 1, for_ed25519=True) key_spend = self.getWalletKey(coin_type, 2, for_ed25519=True) ci.initialiseWallet(key_view, key_spend) @@ -1951,7 +1988,7 @@ class BasicSwap(BaseApp): return self.ci(coin_type).get_fee_rate(conf_target) def estimateWithdrawFee(self, coin_type, fee_rate): - if coin_type == Coins.XMR: + if coin_type in (Coins.XMR, Coins.WOW): # Fee estimate must be manually initiated return None tx_vsize = self.ci(coin_type).getHTLCSpendTxVSize() @@ -1960,7 +1997,7 @@ class BasicSwap(BaseApp): def withdrawCoin(self, coin_type, value, addr_to, subfee: bool) -> str: ci = self.ci(coin_type) - if subfee and coin_type == Coins.XMR: + if subfee and coin_type in (Coins.XMR, Coins.WOW): self.log.info('withdrawCoin sweep all {} to {}'.format(ci.ticker(), addr_to)) else: self.log.info('withdrawCoin {} {} to {} {}'.format(value, ci.ticker(), addr_to, ' subfee' if subfee else '')) @@ -2012,7 +2049,7 @@ class BasicSwap(BaseApp): if c == Coins.PART: ci.setWalletSeedWarning(False) # All keys should be be derived from the Particl mnemonic return True # TODO - if c == Coins.XMR: + if c in (Coins.XMR, Coins.WOW): expect_address = self.getCachedMainWalletAddress(ci) if expect_address is None: self.log.warning('Can\'t find expected main wallet address for coin {}'.format(ci.coin_name())) @@ -2056,7 +2093,7 @@ class BasicSwap(BaseApp): # TODO: How to scan pruned blocks? if not self.checkWalletSeed(coin_type): - if coin_type == Coins.XMR: + if coin_type in (Coins.XMR, Coins.WOW): raise ValueError('TODO: How to reseed XMR wallet?') else: raise ValueError('Wallet seed doesn\'t match expected.') @@ -5865,7 +5902,7 @@ class BasicSwap(BaseApp): kbsl = self.getPathKey(coin_from, coin_to, bid.created_at, xmr_swap.contract_count, KeyTypes.KBSL, for_ed25519) vkbs = ci_to.sumKeys(kbsl, kbsf) - if coin_to == Coins.XMR: + if coin_to == (Coins.XMR, Coins.WOW): address_to = self.getCachedMainWalletAddress(ci_to, session) elif coin_to in (Coins.PART_BLIND, Coins.PART_ANON): address_to = self.getCachedStealthAddressForCoin(coin_to, session) @@ -5932,7 +5969,7 @@ class BasicSwap(BaseApp): vkbs = ci_to.sumKeys(kbsl, kbsf) try: - if offer.coin_to == Coins.XMR: + if offer.coin_to in (Coins.XMR, Coins.WOW): address_to = self.getCachedMainWalletAddress(ci_to, session) elif coin_to in (Coins.PART_BLIND, Coins.PART_ANON): address_to = self.getCachedStealthAddressForCoin(coin_to, session) @@ -6984,7 +7021,7 @@ class BasicSwap(BaseApp): rv['anon_pending'] = walletinfo['unconfirmed_anon'] + walletinfo['immature_anon_balance'] rv['blind_balance'] = walletinfo['blind_balance'] rv['blind_unconfirmed'] = walletinfo['unconfirmed_blind'] - elif coin == Coins.XMR: + elif coin in (Coins.XMR, Coins.WOW): rv['main_address'] = self.getCachedMainWalletAddress(ci) elif coin == Coins.NAV: rv['immature'] = walletinfo['immature_balance'] diff --git a/basicswap/chainparams.py b/basicswap/chainparams.py index 0d967e8..ba8bc58 100644 --- a/basicswap/chainparams.py +++ b/basicswap/chainparams.py @@ -10,6 +10,7 @@ from .util import ( ) XMR_COIN = 10 ** 12 +WOW_COIN = 10 ** 11 class Coins(IntEnum): @@ -21,13 +22,14 @@ class Coins(IntEnum): XMR = 6 PART_BLIND = 7 PART_ANON = 8 - # ZANO = 9 + WOW = 9 # NDAU = 10 PIVX = 11 DASH = 12 FIRO = 13 NAV = 14 LTC_MWEB = 15 + # ZANO = 16 was 9 chainparams = { @@ -247,6 +249,33 @@ chainparams = { 'address_prefix': 18, } }, + Coins.WOW: { + 'name': 'wownero', + 'ticker': 'WOW', + 'client': 'wow', + 'decimal_places': 11, + 'mainnet': { + 'rpcport': 34568, + 'walletrpcport': 34572, # todo + 'min_amount': 100000, + 'max_amount': 10000 * WOW_COIN, + 'address_prefix': 4146, + }, + 'testnet': { + 'rpcport': 44568, + 'walletrpcport': 44572, + 'min_amount': 100000, + 'max_amount': 10000 * WOW_COIN, + 'address_prefix': 4146, + }, + 'regtest': { + 'rpcport': 54568, + 'walletrpcport': 54572, + 'min_amount': 100000, + 'max_amount': 10000 * WOW_COIN, + 'address_prefix': 4146, + } + }, Coins.PIVX: { 'name': 'pivx', 'ticker': 'PIVX', diff --git a/basicswap/config.py b/basicswap/config.py index cad16b1..1fb3511 100644 --- a/basicswap/config.py +++ b/basicswap/config.py @@ -36,3 +36,7 @@ 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) + +WOW_BINDIR = os.path.expanduser(os.getenv('WOW_BINDIR', os.path.join(DEFAULT_TEST_BINDIR, 'wownero'))) +WOWD = os.getenv('WOWD', 'wownerod' + bin_suffix) +WOW_WALLET_RPC = os.getenv('WOW_WALLET_RPC', 'wownero-wallet-rpc' + bin_suffix) diff --git a/basicswap/http_server.py b/basicswap/http_server.py index ba9c6d3..6a2d846 100644 --- a/basicswap/http_server.py +++ b/basicswap/http_server.py @@ -311,6 +311,22 @@ class HttpHandler(BaseHTTPRequestHandler): else: raise ValueError('Unknown RPC variant') result = json.dumps(rv, indent=4) + elif coin_type == Coins.WOW: + ci = swap_client.ci(coin_type) + arr = cmd.split(None, 1) + method = arr[0] + params = json.loads(arr[1]) if len(arr) > 1 else [] + if coin_id == -8: + rv = ci.rpc_wallet(method, params) + elif coin_id == -7: + rv = ci.rpc(method, params) + elif coin_id == -6: + if params == []: + params = None + rv = ci.rpc2(method, params) + else: + raise ValueError('Unknown WOW RPC variant') + result = json.dumps(rv, indent=4) else: if call_type == 'http': ci = swap_client.ci(coin_type) @@ -338,6 +354,7 @@ class HttpHandler(BaseHTTPRequestHandler): coin_available = listAvailableCoins(swap_client, with_variants=False) with_xmr: bool = any(c[0] == Coins.XMR for c in coin_available) + with_wow: bool = any(c[0] == Coins.WOW for c in coin_available) coins = [(str(c[0]) + ',0', c[1]) for c in coin_available if c[0] not in (Coins.XMR, )] if any(c[0] == Coins.DCR for c in coin_available): @@ -348,6 +365,10 @@ class HttpHandler(BaseHTTPRequestHandler): coins.append((str(int(Coins.XMR)) + ',0', 'Monero')) coins.append((str(int(Coins.XMR)) + ',1', 'Monero JSON')) coins.append((str(int(Coins.XMR)) + ',2', 'Monero Wallet')) + if with_wow: + coins.append((str(int(Coins.WOW)) + ',0', 'Wownero')) + coins.append((str(int(Coins.WOW)) + ',1', 'Wownero JSON')) + coins.append((str(int(Coins.WOW)) + ',2', 'Wownero Wallet')) return self.render_template(template, { 'messages': messages, diff --git a/basicswap/interface/wow.py b/basicswap/interface/wow.py new file mode 100644 index 0000000..9d66a45 --- /dev/null +++ b/basicswap/interface/wow.py @@ -0,0 +1,577 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright (c) 2020-2024 tecnovert +# Distributed under the MIT software license, see the accompanying +# file LICENSE or http://www.opensource.org/licenses/mit-license.php. + +import json +import logging + +import basicswap.contrib.ed25519_fast as edf +import basicswap.ed25519_fast_util as edu +import basicswap.util_xmr as wow_util +from coincurve.ed25519 import ( + ed25519_add, + ed25519_get_pubkey, + ed25519_scalar_add, +) +from coincurve.keys import PrivateKey +from coincurve.dleag import ( + dleag_prove, + dleag_verify, + dleag_proof_len, + verify_ed25519_point, +) + +from basicswap.interface import ( + Curves) +from basicswap.util import ( + i2b, b2i, b2h, + dumpj, + ensure, + make_int, + TemporaryError) +from basicswap.util.network import ( + is_private_ip_address) +from basicswap.rpc_wow import ( + make_wow_rpc_func, + make_wow_rpc2_func) +from basicswap.chainparams import WOW_COIN, CoinInterface, Coins + + +class WOWInterface(CoinInterface): + @staticmethod + def curve_type(): + return Curves.ed25519 + + @staticmethod + def coin_type(): + return Coins.WOW + + @staticmethod + def COIN(): + return WOW_COIN + + @staticmethod + def exp() -> int: + return 11 + + @staticmethod + def nbk() -> int: + return 32 + + @staticmethod + def nbK() -> int: # No. of bytes requires to encode a public key + return 32 + + @staticmethod + def depth_spendable() -> int: + return 10 + + @staticmethod + def xmr_swap_a_lock_spend_tx_vsize() -> int: + raise ValueError('Not possible') + + @staticmethod + def xmr_swap_b_lock_spend_tx_vsize() -> int: + # TODO: Estimate with ringsize + return 1604 + + def __init__(self, coin_settings, network, swap_client=None): + super().__init__(network) + + self._addr_prefix = self.chainparams_network()['address_prefix'] + + self.blocks_confirmed = coin_settings['blocks_confirmed'] + self._restore_height = coin_settings.get('restore_height', 0) + self.setFeePriority(coin_settings.get('fee_priority', 0)) + self._sc = swap_client + self._log = self._sc.log if self._sc and self._sc.log else logging + self._wallet_password = None + self._have_checked_seed = False + + daemon_login = None + if coin_settings.get('rpcuser', '') != '': + daemon_login = (coin_settings.get('rpcuser', ''), coin_settings.get('rpcpassword', '')) + + rpchost = coin_settings.get('rpchost', '127.0.0.1') + proxy_host = None + proxy_port = None + # Connect to the daemon over a proxy if not running locally + if swap_client: + chain_client_settings = swap_client.getChainClientSettings(self.coin_type()) + manage_daemon: bool = chain_client_settings['manage_daemon'] + if swap_client.use_tor_proxy: + if manage_daemon is False: + log_str: str = '' + have_cc_tor_opt = 'use_tor' in chain_client_settings + if have_cc_tor_opt and chain_client_settings['use_tor'] is False: + log_str = ' bypassing proxy (use_tor false for WOW)' + elif have_cc_tor_opt is False and is_private_ip_address(rpchost): + log_str = ' bypassing proxy (private ip address)' + else: + proxy_host = swap_client.tor_proxy_host + proxy_port = swap_client.tor_proxy_port + log_str = f' through proxy at {proxy_host}' + self._log.info(f'Connecting to remote {self.coin_name()} daemon at {rpchost}{log_str}.') + else: + self._log.info(f'Not connecting to local {self.coin_name()} daemon through proxy.') + elif manage_daemon is False: + self._log.info(f'Connecting to remote {self.coin_name()} daemon at {rpchost}.') + + self._rpctimeout = coin_settings.get('rpctimeout', 60) + self._walletrpctimeout = coin_settings.get('walletrpctimeout', 120) + self._walletrpctimeoutlong = coin_settings.get('walletrpctimeoutlong', 600) + + self.rpc = make_wow_rpc_func(coin_settings['rpcport'], daemon_login, host=rpchost, proxy_host=proxy_host, proxy_port=proxy_port, default_timeout=self._rpctimeout, tag='Node(j) ') + self.rpc2 = make_wow_rpc2_func(coin_settings['rpcport'], daemon_login, host=rpchost, proxy_host=proxy_host, proxy_port=proxy_port, default_timeout=self._rpctimeout, tag='Node ') # non-json endpoint + self.rpc_wallet = make_wow_rpc_func(coin_settings['walletrpcport'], coin_settings['walletrpcauth'], host=coin_settings.get('walletrpchost', '127.0.0.1'), default_timeout=self._walletrpctimeout, tag='Wallet ') + + def checkWallets(self) -> int: + return 1 + + def setFeePriority(self, new_priority): + ensure(new_priority >= 0 and new_priority < 4, 'Invalid fee_priority value') + self._fee_priority = new_priority + + def setWalletFilename(self, wallet_filename): + self._wallet_filename = wallet_filename + + def createWallet(self, params): + if self._wallet_password is not None: + params['password'] = self._wallet_password + rv = self.rpc_wallet('generate_from_keys', params) + self._log.info('generate_from_keys %s', dumpj(rv)) + + def openWallet(self, filename): + params = {'filename': filename} + if self._wallet_password is not None: + params['password'] = self._wallet_password + + try: + # Can't reopen the same wallet in windows, !is_keys_file_locked() + self.rpc_wallet('close_wallet') + except Exception: + pass + self.rpc_wallet('open_wallet', params) + + def initialiseWallet(self, key_view, key_spend, restore_height=None): + with self._mx_wallet: + try: + self.openWallet(self._wallet_filename) + # TODO: Check address + return # Wallet exists + except Exception as e: + pass + + Kbv = self.getPubkey(key_view) + Kbs = self.getPubkey(key_spend) + address_b58 = wow_util.encode_address(Kbv, Kbs, self._addr_prefix) + + params = { + 'filename': self._wallet_filename, + 'address': address_b58, + 'viewkey': b2h(key_view[::-1]), + 'spendkey': b2h(key_spend[::-1]), + 'restore_height': self._restore_height, + } + self.createWallet(params) + self.openWallet(self._wallet_filename) + + def ensureWalletExists(self) -> None: + with self._mx_wallet: + self.openWallet(self._wallet_filename) + + def testDaemonRPC(self, with_wallet=True) -> None: + self.rpc_wallet('get_languages') + + def getDaemonVersion(self): + return self.rpc_wallet('get_version')['version'] + + def getBlockchainInfo(self): + get_height = self.rpc2('get_height', timeout=self._rpctimeout) + rv = { + 'blocks': get_height['height'], + 'verificationprogress': 0.0, + } + + try: + # get_block_count.block_count is how many blocks are in the longest chain known to the node. + # get_block_count returns "Internal error" if bootstrap-daemon is active + if get_height['untrusted'] is True: + rv['bootstrapping'] = True + get_info = self.rpc2('get_info', timeout=self._rpctimeout) + if 'height_without_bootstrap' in get_info: + rv['blocks'] = get_info['height_without_bootstrap'] + + rv['known_block_count'] = get_info['height'] + if rv['known_block_count'] > rv['blocks']: + rv['verificationprogress'] = rv['blocks'] / rv['known_block_count'] + else: + rv['known_block_count'] = self.rpc('get_block_count', timeout=self._rpctimeout)['count'] + rv['verificationprogress'] = rv['blocks'] / rv['known_block_count'] + except Exception as e: + self._log.warning('WOW get_block_count failed with: %s', str(e)) + rv['verificationprogress'] = 0.0 + + return rv + + def getChainHeight(self): + return self.rpc2('get_height', timeout=self._rpctimeout)['height'] + + def getWalletInfo(self): + with self._mx_wallet: + try: + self.openWallet(self._wallet_filename) + except Exception as e: + if 'Failed to open wallet' in str(e): + rv = {'encrypted': True, 'locked': True, 'balance': 0, 'unconfirmed_balance': 0} + return rv + raise e + + rv = {} + self.rpc_wallet('refresh') + balance_info = self.rpc_wallet('get_balance') + + rv['balance'] = self.format_amount(balance_info['unlocked_balance']) + rv['unconfirmed_balance'] = self.format_amount(balance_info['balance'] - balance_info['unlocked_balance']) + rv['encrypted'] = False if self._wallet_password is None else True + rv['locked'] = False + return rv + + def walletRestoreHeight(self): + return self._restore_height + + def getMainWalletAddress(self) -> str: + with self._mx_wallet: + self.openWallet(self._wallet_filename) + return self.rpc_wallet('get_address')['address'] + + def getNewAddress(self, placeholder) -> str: + with self._mx_wallet: + self.openWallet(self._wallet_filename) + new_address = self.rpc_wallet('create_address', {'account_index': 0})['address'] + self.rpc_wallet('store') + return new_address + + def get_fee_rate(self, conf_target: int = 2): + # fees - array of unsigned int; Represents the base fees at different priorities [slow, normal, fast, fastest]. + fee_est = self.rpc('get_fee_estimate') + if conf_target <= 1: + conf_target = 1 # normal + else: + conf_target = 0 # slow + fee_per_k_bytes = fee_est['fees'][conf_target] * 1000 + + return float(self.format_amount(fee_per_k_bytes)), 'get_fee_estimate' + + def getNewSecretKey(self) -> bytes: + # Note: Returned bytes are in big endian order + return i2b(edu.get_secret()) + + def pubkey(self, key: bytes) -> bytes: + return edf.scalarmult_B(key) + + def encodeKey(self, vk: bytes) -> str: + return vk[::-1].hex() + + def decodeKey(self, k_hex: str) -> bytes: + return bytes.fromhex(k_hex)[::-1] + + def encodePubkey(self, pk: bytes) -> str: + return edu.encodepoint(pk) + + def decodePubkey(self, pke): + return edf.decodepoint(pke) + + def getPubkey(self, privkey): + return ed25519_get_pubkey(privkey) + + def getAddressFromKeys(self, key_view: bytes, key_spend: bytes) -> str: + pk_view = self.getPubkey(key_view) + pk_spend = self.getPubkey(key_spend) + return wow_util.encode_address(pk_view, pk_spend, self._addr_prefix) + + def verifyKey(self, k: int) -> bool: + i = b2i(k) + return (i < edf.l and i > 8) + + def verifyPubkey(self, pubkey_bytes): + # Calls ed25519_decode_check_point() in secp256k1 + # Checks for small order + return verify_ed25519_point(pubkey_bytes) + + def proveDLEAG(self, key: bytes) -> bytes: + privkey = PrivateKey(key) + return dleag_prove(privkey) + + def verifyDLEAG(self, dleag_bytes: bytes) -> bool: + return dleag_verify(dleag_bytes) + + def lengthDLEAG(self) -> int: + return dleag_proof_len() + + def sumKeys(self, ka: bytes, kb: bytes) -> bytes: + return ed25519_scalar_add(ka, kb) + + def sumPubkeys(self, Ka: bytes, Kb: bytes) -> bytes: + return ed25519_add(Ka, Kb) + + def encodeSharedAddress(self, Kbv: bytes, Kbs: bytes) -> str: + return wow_util.encode_address(Kbv, Kbs, self._addr_prefix) + + def publishBLockTx(self, kbv: bytes, Kbs: bytes, output_amount: int, feerate: int, unlock_time: int = 0) -> bytes: + with self._mx_wallet: + self.openWallet(self._wallet_filename) + self.rpc_wallet('refresh') + + Kbv = self.getPubkey(kbv) + shared_addr = wow_util.encode_address(Kbv, Kbs, self._addr_prefix) + + params = {'destinations': [{'amount': output_amount, 'address': shared_addr}], 'unlock_time': unlock_time} + if self._fee_priority > 0: + params['priority'] = self._fee_priority + rv = self.rpc_wallet('transfer', params) + self._log.info('publishBLockTx %s to address_b58 %s', rv['tx_hash'], shared_addr) + tx_hash = bytes.fromhex(rv['tx_hash']) + + return tx_hash + + def findTxB(self, kbv, Kbs, cb_swap_value, cb_block_confirmed, restore_height, bid_sender): + with self._mx_wallet: + Kbv = self.getPubkey(kbv) + address_b58 = wow_util.encode_address(Kbv, Kbs, self._addr_prefix) + + kbv_le = kbv[::-1] + params = { + 'restore_height': restore_height, + 'filename': address_b58, + 'address': address_b58, + 'viewkey': b2h(kbv_le), + } + + try: + self.openWallet(address_b58) + except Exception as e: + self.createWallet(params) + self.openWallet(address_b58) + + self.rpc_wallet('refresh', timeout=self._walletrpctimeoutlong) + + ''' + # Debug + try: + current_height = self.rpc_wallet('get_height')['height'] + self._log.info('findTxB WOW current_height %d\nAddress: %s', current_height, address_b58) + except Exception as e: + self._log.info('rpc failed %s', str(e)) + current_height = None # If the transfer is available it will be deep enough + # and (current_height is None or current_height - transfer['block_height'] > cb_block_confirmed): + ''' + params = {'transfer_type': 'available'} + transfers = self.rpc_wallet('incoming_transfers', params) + rv = None + if 'transfers' in transfers: + for transfer in transfers['transfers']: + # unlocked <- wallet->is_transfer_unlocked() checks unlock_time and CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE + if not transfer['unlocked']: + full_tx = self.rpc_wallet('get_transfer_by_txid', {'txid': transfer['tx_hash']}) + unlock_time = full_tx['transfer']['unlock_time'] + if unlock_time != 0: + self._log.warning('Coin b lock txn is locked: {}, unlock_time {}'.format(transfer['tx_hash'], unlock_time)) + rv = -1 + continue + if transfer['amount'] == cb_swap_value: + return {'txid': transfer['tx_hash'], 'amount': transfer['amount'], 'height': 0 if 'block_height' not in transfer else transfer['block_height']} + else: + self._log.warning('Incorrect amount detected for coin b lock txn: {}'.format(transfer['tx_hash'])) + rv = -1 + return rv + + def findTxnByHash(self, txid): + with self._mx_wallet: + self.openWallet(self._wallet_filename) + self.rpc_wallet('refresh', timeout=self._walletrpctimeoutlong) + + try: + current_height = self.rpc2('get_height', timeout=self._rpctimeout)['height'] + self._log.info('findTxnByHash WOW current_height %d\nhash: %s', current_height, txid) + except Exception as e: + self._log.info('rpc failed %s', str(e)) + current_height = None # If the transfer is available it will be deep enough + + params = {'transfer_type': 'available'} + rv = self.rpc_wallet('incoming_transfers', params) + if 'transfers' in rv: + for transfer in rv['transfers']: + if transfer['tx_hash'] == txid \ + and (current_height is None or current_height - transfer['block_height'] > self.blocks_confirmed): + return {'txid': transfer['tx_hash'], 'amount': transfer['amount'], 'height': transfer['block_height']} + + return None + + def spendBLockTx(self, chain_b_lock_txid: bytes, address_to: str, kbv: bytes, kbs: bytes, cb_swap_value: int, b_fee_rate: int, restore_height: int, spend_actual_balance: bool = False) -> bytes: + ''' + Notes: + "Error: No unlocked balance in the specified subaddress(es)" can mean not enough funds after tx fee. + ''' + with self._mx_wallet: + Kbv = self.getPubkey(kbv) + Kbs = self.getPubkey(kbs) + address_b58 = wow_util.encode_address(Kbv, Kbs, self._addr_prefix) + + wallet_filename = address_b58 + '_spend' + + params = { + 'filename': wallet_filename, + 'address': address_b58, + 'viewkey': b2h(kbv[::-1]), + 'spendkey': b2h(kbs[::-1]), + 'restore_height': restore_height, + } + + try: + self.openWallet(wallet_filename) + except Exception as e: + self.createWallet(params) + self.openWallet(wallet_filename) + + self.rpc_wallet('refresh') + rv = self.rpc_wallet('get_balance') + if rv['balance'] < cb_swap_value: + self._log.warning('Balance is too low, checking for existing spend.') + txns = self.rpc_wallet('get_transfers', {'out': True}) + if 'out' in txns: + txns = txns['out'] + if len(txns) > 0: + txid = txns[0]['txid'] + self._log.warning(f'spendBLockTx detected spending tx: {txid}.') + if txns[0]['address'] == address_b58: + return bytes.fromhex(txid) + + self._log.error('wallet {} balance {}, expected {}'.format(wallet_filename, rv['balance'], cb_swap_value)) + + if not spend_actual_balance: + raise TemporaryError('Invalid balance') + + if spend_actual_balance and rv['balance'] != cb_swap_value: + self._log.warning('Spending actual balance {}, not swap value {}.'.format(rv['balance'], cb_swap_value)) + cb_swap_value = rv['balance'] + if rv['unlocked_balance'] < cb_swap_value: + self._log.error('wallet {} balance {}, expected {}, blocks_to_unlock {}'.format(wallet_filename, rv['unlocked_balance'], cb_swap_value, rv['blocks_to_unlock'])) + raise TemporaryError('Invalid unlocked_balance') + + params = {'address': address_to} + if self._fee_priority > 0: + params['priority'] = self._fee_priority + + rv = self.rpc_wallet('sweep_all', params) + self._log.debug('sweep_all {}'.format(json.dumps(rv))) + + return bytes.fromhex(rv['tx_hash_list'][0]) + + def withdrawCoin(self, value, addr_to: str, sweepall: bool, estimate_fee: bool = False) -> str: + with self._mx_wallet: + self.openWallet(self._wallet_filename) + self.rpc_wallet('refresh') + + if sweepall: + balance = self.rpc_wallet('get_balance') + if balance['balance'] != balance['unlocked_balance']: + raise ValueError('Balance must be fully confirmed to use sweep all.') + self._log.info('WOW {} sweep_all.'.format('estimate fee' if estimate_fee else 'withdraw')) + self._log.debug('WOW balance: {}'.format(balance['balance'])) + params = {'address': addr_to, 'do_not_relay': estimate_fee} + if self._fee_priority > 0: + params['priority'] = self._fee_priority + rv = self.rpc_wallet('sweep_all', params) + if estimate_fee: + return {'num_txns': len(rv['fee_list']), 'sum_amount': sum(rv['amount_list']), 'sum_fee': sum(rv['fee_list']), 'sum_weight': sum(rv['weight_list'])} + return rv['tx_hash_list'][0] + + value_sats: int = make_int(value, self.exp()) + params = {'destinations': [{'amount': value_sats, 'address': addr_to}], 'do_not_relay': estimate_fee} + if self._fee_priority > 0: + params['priority'] = self._fee_priority + rv = self.rpc_wallet('transfer', params) + if estimate_fee: + return {'num_txns': 1, 'sum_amount': rv['amount'], 'sum_fee': rv['fee'], 'sum_weight': rv['weight']} + return rv['tx_hash'] + + def estimateFee(self, value: int, addr_to: str, sweepall: bool) -> str: + return self.withdrawCoin(value, addr_to, sweepall, estimate_fee=True) + + def showLockTransfers(self, kbv, Kbs, restore_height): + with self._mx_wallet: + try: + Kbv = self.getPubkey(kbv) + address_b58 = wow_util.encode_address(Kbv, Kbs, self._addr_prefix) + wallet_file = address_b58 + '_spend' + try: + self.openWallet(wallet_file) + except Exception: + wallet_file = address_b58 + try: + self.openWallet(wallet_file) + except Exception: + self._log.info(f'showLockTransfers trying to create wallet for address {address_b58}.') + kbv_le = kbv[::-1] + params = { + 'restore_height': restore_height, + 'filename': address_b58, + 'address': address_b58, + 'viewkey': b2h(kbv_le), + } + self.createWallet(params) + self.openWallet(address_b58) + + self.rpc_wallet('refresh') + + rv = self.rpc_wallet('get_transfers', {'in': True, 'out': True, 'pending': True, 'failed': True}) + rv['filename'] = wallet_file + return rv + except Exception as e: + return {'error': str(e)} + + def getSpendableBalance(self) -> int: + with self._mx_wallet: + self.openWallet(self._wallet_filename) + + self.rpc_wallet('refresh') + balance_info = self.rpc_wallet('get_balance') + return balance_info['unlocked_balance'] + + def changeWalletPassword(self, old_password, new_password): + self._log.info('changeWalletPassword - {}'.format(self.ticker())) + orig_password = self._wallet_password + if old_password != '': + self._wallet_password = old_password + try: + self.openWallet(self._wallet_filename) + self.rpc_wallet('change_wallet_password', {'old_password': old_password, 'new_password': new_password}) + except Exception as e: + self._wallet_password = orig_password + raise e + + def unlockWallet(self, password: str) -> None: + self._log.info('unlockWallet - {}'.format(self.ticker())) + self._wallet_password = password + + if not self._have_checked_seed: + self._sc.checkWalletSeed(self.coin_type()) + + def lockWallet(self) -> None: + self._log.info('lockWallet - {}'.format(self.ticker())) + self._wallet_password = None + + def isAddressMine(self, address): + # TODO + return True + + def ensureFunds(self, amount: int) -> None: + if self.getSpendableBalance() < amount: + raise ValueError('Balance too low') + + def getTransaction(self, txid: bytes): + return self.rpc2('get_transactions', {'txs_hashes': [txid.hex(), ]}) diff --git a/basicswap/js_server.py b/basicswap/js_server.py index d3ff2e4..0f8d303 100644 --- a/basicswap/js_server.py +++ b/basicswap/js_server.py @@ -54,7 +54,7 @@ def withdraw_coin(swap_client, coin_type, post_string, is_json): post_data = getFormData(post_string, is_json) address = get_data_entry(post_data, 'address') - if coin_type == Coins.XMR: + if coin_type in (Coins.XMR, Coins.WOW): value = None sweepall = get_data_entry(post_data, 'sweepall') if not isinstance(sweepall, bool): @@ -74,7 +74,7 @@ def withdraw_coin(swap_client, coin_type, post_string, is_json): elif coin_type == Coins.LTC: type_from = get_data_entry_or(post_data, 'type_from', 'plain') txid_hex = swap_client.withdrawLTC(type_from, value, address, subfee) - elif coin_type == Coins.XMR: + elif coin_type in (Coins.XMR, Coins.WOW): txid_hex = swap_client.withdrawCoin(coin_type, value, address, sweepall) else: txid_hex = swap_client.withdrawCoin(coin_type, value, address, subfee) @@ -685,7 +685,7 @@ def js_getcoinseed(self, url_split, post_string, is_json) -> bytes: raise ValueError('Particl wallet seed is set from the Basicswap mnemonic.') ci = swap_client.ci(coin) - if coin == Coins.XMR: + if coin in (Coins.XMR, Coins.WOW): key_view = swap_client.getWalletKey(coin, 1, for_ed25519=True) key_spend = swap_client.getWalletKey(coin, 2, for_ed25519=True) address = ci.getAddressFromKeys(key_view, key_spend) diff --git a/basicswap/rpc_wow.py b/basicswap/rpc_wow.py new file mode 100644 index 0000000..21ccac3 --- /dev/null +++ b/basicswap/rpc_wow.py @@ -0,0 +1,258 @@ +# -*- coding: utf-8 -*- + +import os +import json +import socks +import time +import urllib +import hashlib +from xmlrpc.client import ( + Fault, + Transport, + SafeTransport, +) +from sockshandler import SocksiPyConnection +from .util import jsonDecimal + + +class SocksTransport(Transport): + + def set_proxy(self, proxy_host, proxy_port): + self.proxy_host = proxy_host + self.proxy_port = proxy_port + + self.proxy_type = socks.PROXY_TYPE_SOCKS5 + self.proxy_rdns = True + self.proxy_username = None + self.proxy_password = None + + def make_connection(self, host): + # return an existing connection if possible. This allows + # HTTP/1.1 keep-alive. + if self._connection and host == self._connection[0]: + return self._connection[1] + # create a HTTP connection object from a host descriptor + chost, self._extra_headers, x509 = self.get_host_info(host) + self._connection = host, SocksiPyConnection(self.proxy_type, self.proxy_host, self.proxy_port, self.proxy_rdns, self.proxy_username, self.proxy_password, chost) + return self._connection[1] + + +class JsonrpcDigest(): + # __getattr__ complicates extending ServerProxy + def __init__(self, uri, transport=None, encoding=None, verbose=False, + allow_none=False, use_datetime=False, use_builtin_types=False, + *, context=None): + + parsed = urllib.parse.urlparse(uri) + if parsed.scheme not in ('http', 'https'): + raise OSError('unsupported XML-RPC protocol') + self.__host = parsed.netloc + self.__handler = parsed.path + + if transport is None: + handler = SafeTransport if parsed.scheme == 'https' else Transport + extra_kwargs = {} + transport = handler(use_datetime=use_datetime, + use_builtin_types=use_builtin_types, + **extra_kwargs) + self.__transport = transport + + self.__encoding = encoding or 'utf-8' + self.__verbose = verbose + self.__allow_none = allow_none + + self.__request_id = 0 + + def close(self): + if self.__transport is not None: + self.__transport.close() + + def request_id(self): + return self.__request_id + + def post_request(self, method, params, timeout=None): + try: + connection = self.__transport.make_connection(self.__host) + if timeout: + connection.timeout = timeout + headers = self.__transport._extra_headers[:] + + connection.putrequest('POST', self.__handler) + headers.append(('Content-Type', 'application/json')) + headers.append(('User-Agent', 'jsonrpc')) + self.__transport.send_headers(connection, headers) + self.__transport.send_content(connection, '' if params is None else json.dumps(params, default=jsonDecimal).encode('utf-8')) + self.__request_id += 1 + + resp = connection.getresponse() + return resp.read() + + except Fault: + raise + except Exception: + self.__transport.close() + raise + + def json_request(self, request_body, username='', password='', timeout=None): + try: + connection = self.__transport.make_connection(self.__host) + if timeout: + connection.timeout = timeout + + headers = self.__transport._extra_headers[:] + + connection.putrequest('POST', self.__handler) + headers.append(('Content-Type', 'application/json')) + headers.append(('Connection', 'keep-alive')) + self.__transport.send_headers(connection, headers) + self.__transport.send_content(connection, json.dumps(request_body, default=jsonDecimal).encode('utf-8') if request_body else '') + resp = connection.getresponse() + + if resp.status == 401: + resp_headers = resp.getheaders() + v = resp.read() + + algorithm = '' + realm = '' + nonce = '' + for h in resp_headers: + if h[0] != 'WWW-authenticate': + continue + fields = h[1].split(',') + for f in fields: + key, value = f.split('=', 1) + if key == 'algorithm' and value != 'MD5': + break + if key == 'realm': + realm = value.strip('"') + if key == 'nonce': + nonce = value.strip('"') + if realm != '' and nonce != '': + break + + if realm == '' or nonce == '': + raise ValueError('Authenticate header not found.') + + path = self.__handler + HA1 = hashlib.md5(f'{username}:{realm}:{password}'.encode('utf-8')).hexdigest() + + http_method = 'POST' + HA2 = hashlib.md5(f'{http_method}:{path}'.encode('utf-8')).hexdigest() + + ncvalue = '{:08x}'.format(1) + s = ncvalue.encode('utf-8') + s += nonce.encode('utf-8') + s += time.ctime().encode('utf-8') + s += os.urandom(8) + cnonce = (hashlib.sha1(s).hexdigest()[:16]) + + # MD5-SESS + HA1 = hashlib.md5(f'{HA1}:{nonce}:{cnonce}'.encode('utf-8')).hexdigest() + + respdig = hashlib.md5(f'{HA1}:{nonce}:{ncvalue}:{cnonce}:auth:{HA2}'.encode('utf-8')).hexdigest() + + header_value = f'Digest username="{username}", realm="{realm}", nonce="{nonce}", uri="{path}", response="{respdig}", algorithm="MD5-sess", qop="auth", nc={ncvalue}, cnonce="{cnonce}"' + headers = self.__transport._extra_headers[:] + headers.append(('Authorization', header_value)) + + connection.putrequest('POST', self.__handler) + headers.append(('Content-Type', 'application/json')) + headers.append(('Connection', 'keep-alive')) + self.__transport.send_headers(connection, headers) + self.__transport.send_content(connection, json.dumps(request_body, default=jsonDecimal).encode('utf-8') if request_body else '') + resp = connection.getresponse() + + self.__request_id += 1 + return resp.read() + + except Fault: + raise + except Exception: + self.__transport.close() + raise + + +def callrpc_wow(rpc_port, method, params=[], rpc_host='127.0.0.1', path='json_rpc', auth=None, timeout=120, transport=None, tag=''): + # auth is a tuple: (username, password) + try: + if rpc_host.count('://') > 0: + url = '{}:{}/{}'.format(rpc_host, rpc_port, path) + else: + url = 'http://{}:{}/{}'.format(rpc_host, rpc_port, path) + + x = JsonrpcDigest(url, transport=transport) + request_body = { + 'method': method, + 'params': params, + 'jsonrpc': '2.0', + 'id': x.request_id() + } + if auth: + v = x.json_request(request_body, username=auth[0], password=auth[1], timeout=timeout) + else: + v = x.json_request(request_body, timeout=timeout) + x.close() + r = json.loads(v.decode('utf-8')) + except Exception as ex: + raise ValueError('{}RPC Server Error: {}'.format(tag, str(ex))) + + if 'error' in r and r['error'] is not None: + raise ValueError(tag + 'RPC error ' + str(r['error'])) + + return r['result'] + + +def callrpc_wow2(rpc_port: int, method: str, params=None, auth=None, rpc_host='127.0.0.1', timeout=120, transport=None, tag=''): + try: + if rpc_host.count('://') > 0: + url = '{}:{}/{}'.format(rpc_host, rpc_port, method) + else: + url = 'http://{}:{}/{}'.format(rpc_host, rpc_port, method) + + x = JsonrpcDigest(url, transport=transport) + if auth: + v = x.json_request(params, username=auth[0], password=auth[1], timeout=timeout) + else: + v = x.json_request(params, timeout=timeout) + x.close() + r = json.loads(v.decode('utf-8')) + except Exception as ex: + raise ValueError('{}RPC Server Error: {}'.format(tag, str(ex))) + + return r + + +def make_wow_rpc2_func(port, auth, host='127.0.0.1', proxy_host=None, proxy_port=None, default_timeout=120, tag=''): + port = port + auth = auth + host = host + transport = None + default_timeout = default_timeout + tag = tag + + if proxy_host: + transport = SocksTransport() + transport.set_proxy(proxy_host, proxy_port) + + def rpc_func(method, params=None, wallet=None, timeout=default_timeout): + nonlocal port, auth, host, transport, tag + return callrpc_wow2(port, method, params, auth=auth, rpc_host=host, timeout=timeout, transport=transport, tag=tag) + return rpc_func + + +def make_wow_rpc_func(port, auth, host='127.0.0.1', proxy_host=None, proxy_port=None, default_timeout=120, tag=''): + port = port + auth = auth + host = host + transport = None + default_timeout = default_timeout + tag = tag + + if proxy_host: + transport = SocksTransport() + transport.set_proxy(proxy_host, proxy_port) + + def rpc_func(method, params=None, wallet=None, timeout=default_timeout): + nonlocal port, auth, host, transport, tag + return callrpc_wow(port, method, params, rpc_host=host, auth=auth, timeout=timeout, transport=transport, tag=tag) + return rpc_func diff --git a/basicswap/templates/offer_new_1.html b/basicswap/templates/offer_new_1.html index a86b5ec..14d4f87 100644 --- a/basicswap/templates/offer_new_1.html +++ b/basicswap/templates/offer_new_1.html @@ -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 */, '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 */]; 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)) { diff --git a/basicswap/templates/settings.html b/basicswap/templates/settings.html index 1d3bdec..ff4a2c6 100644 --- a/basicswap/templates/settings.html +++ b/basicswap/templates/settings.html @@ -105,7 +105,7 @@ {% endif %} {% if c.manage_daemon is defined %} - {% if c.name == 'monero' %} + {% if c.name in ('wownero', 'monero') %} Manage Daemon @@ -175,7 +175,7 @@ - {% if c.name == 'monero' %} + {% if c.name in ('wownero', 'monero') %} Transaction Fee Priority diff --git a/basicswap/templates/wallet.html b/basicswap/templates/wallet.html index 3ed9b96..75499ee 100644 --- a/basicswap/templates/wallet.html +++ b/basicswap/templates/wallet.html @@ -266,8 +266,8 @@ {% endif %} {# / PART #} - {% if w.cid == '6' %} - {# XMR #} + {% if w.cid in '6, 9' %} + {# XMR | WOW #}
@@ -412,8 +412,8 @@ }); {% endif %} - {% if w.cid == '6' %} - {# XMR #} + {% if w.cid in '6, 9' %} + {# XMR | WOW #} {% include 'footer.html' %} - \ No newline at end of file + diff --git a/basicswap/templates/wallets.html b/basicswap/templates/wallets.html index f15a3eb..304c2a7 100644 --- a/basicswap/templates/wallets.html +++ b/basicswap/templates/wallets.html @@ -213,11 +213,11 @@ const coinNameToSymbol = { 'Particl Blind': 'PART', 'Particl Anon': 'PART', 'Monero': 'XMR', + 'Wownero': 'WOW', 'Litecoin': 'LTC', 'Firo': 'FIRO', 'Dash': 'DASH', 'PIVX': 'PIVX', - 'Wownero': 'WOW', 'Decred': 'DCR', 'Zano': 'ZANO', }; diff --git a/basicswap/ui/page_offers.py b/basicswap/ui/page_offers.py index a8e2662..94e9906 100644 --- a/basicswap/ui/page_offers.py +++ b/basicswap/ui/page_offers.py @@ -89,7 +89,7 @@ def parseOfferFormData(swap_client, form_data, page_data, options={}): page_data['coin_from'] = getCoinType(get_data_entry(form_data, 'coin_from')) coin_from = Coins(page_data['coin_from']) ci_from = swap_client.ci(coin_from) - if coin_from != Coins.XMR: + if coin_from not in (Coins.XMR, Coins.WOW): page_data['fee_from_conf'] = ci_from._conf_target # Set default value parsed_data['coin_from'] = coin_from except Exception: @@ -99,7 +99,7 @@ def parseOfferFormData(swap_client, form_data, page_data, options={}): page_data['coin_to'] = getCoinType(get_data_entry(form_data, 'coin_to')) coin_to = Coins(page_data['coin_to']) ci_to = swap_client.ci(coin_to) - if coin_to != Coins.XMR: + if coin_to not in (Coins.XMR, Coins.WOW): page_data['fee_to_conf'] = ci_to._conf_target # Set default value parsed_data['coin_to'] = coin_to except Exception: @@ -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_to'] in (Coins.XMR, Coins.PART_ANON): + elif parsed_data['coin_to'] in (Coins.XMR, Coins.WOW, Coins.PART_ANON): parsed_data['swap_type'] = strSwapType(SwapTypes.XMR_SWAP) swap_type = SwapTypes.XMR_SWAP else: @@ -243,7 +243,7 @@ def parseOfferFormData(swap_client, form_data, page_data, options={}): page_data['amt_from_lock_spend_tx_fee'] = ci_from.format_amount(lock_spend_tx_fee // ci_from.COIN()) page_data['tla_from'] = ci_from.ticker() - if ci_to == Coins.XMR: + if ci_to in (Coins.XMR, Coins.WOW): if have_data_entry(form_data, 'fee_rate_to'): page_data['to_fee_override'] = get_data_entry(form_data, 'fee_rate_to') parsed_data['to_fee_override'] = page_data['to_fee_override'] @@ -267,7 +267,7 @@ def postNewOfferFromParsed(swap_client, parsed_data): if 'swap_type' in parsed_data: str_swap_type = parsed_data['swap_type'].lower() swap_type = swap_type_from_string(str_swap_type) - elif parsed_data['coin_to'] in (Coins.XMR, Coins.PART_ANON): + elif parsed_data['coin_to'] in (Coins.XMR, Coins.WOW, Coins.PART_ANON): swap_type = SwapTypes.XMR_SWAP if swap_type == SwapTypes.XMR_SWAP: diff --git a/basicswap/ui/page_settings.py b/basicswap/ui/page_settings.py index ff688b0..50e538e 100644 --- a/basicswap/ui/page_settings.py +++ b/basicswap/ui/page_settings.py @@ -57,7 +57,7 @@ def page_settings(self, url_split, post_string): for name, c in swap_client.settings['chainclients'].items(): if have_data_entry(form_data, 'apply_' + name): data = {'lookups': get_data_entry(form_data, 'lookups_' + name)} - if name == 'monero': + if name in ('monero', 'wownero'): data['fee_priority'] = int(get_data_entry(form_data, 'fee_priority_' + name)) data['manage_daemon'] = True if get_data_entry(form_data, 'managedaemon_' + name) == 'true' else False data['rpchost'] = get_data_entry(form_data, 'rpchost_' + name) @@ -104,7 +104,7 @@ def page_settings(self, url_split, post_string): 'manage_daemon': c.get('manage_daemon', 'Unknown'), 'connection_type': c.get('connection_type', 'Unknown'), }) - if name == 'monero': + if name in ('monero', 'wownero'): chains_formatted[-1]['fee_priority'] = c.get('fee_priority', 0) chains_formatted[-1]['manage_wallet_daemon'] = c.get('manage_wallet_daemon', 'Unknown') chains_formatted[-1]['rpchost'] = c.get('rpchost', 'localhost') diff --git a/basicswap/ui/page_wallet.py b/basicswap/ui/page_wallet.py index 643b562..d4fa07d 100644 --- a/basicswap/ui/page_wallet.py +++ b/basicswap/ui/page_wallet.py @@ -176,7 +176,7 @@ def page_wallet(self, url_split, post_string): if estimate_fee and withdraw: err_messages.append('Estimate fee and withdraw can\'t be used together.') - if estimate_fee and coin_id not in (Coins.XMR, ): + if estimate_fee and coin_id not in (Coins.XMR, Coins.WOW): ci = swap_client.ci(coin_id) ticker: str = ci.ticker() err_messages.append(f'Estimate fee unavailable for {ticker}.') @@ -206,7 +206,7 @@ def page_wallet(self, url_split, post_string): elif coin_id == Coins.LTC: txid = swap_client.withdrawLTC(type_from, value, address, subfee) messages.append('Withdrew {} {} (from {}) to address {}
In txid: {}'.format(value, ticker, type_from, address, txid)) - elif coin_id == Coins.XMR: + elif coin_id in (Coins.XMR, Coins.WOW): if estimate_fee: fee_estimate = ci.estimateFee(value, address, sweepall) suffix = 's' if fee_estimate['num_txns'] > 1 else '' @@ -281,7 +281,7 @@ def page_wallet(self, url_split, post_string): wallet_data['est_fee'] = 'Unknown' if est_fee is None else ci.format_amount(int(est_fee * ci.COIN())) wallet_data['deposit_address'] = w.get('deposit_address', 'Refresh necessary') - if k == Coins.XMR: + if k in (Coins.XMR, Coins.WOW): wallet_data['main_address'] = w.get('main_address', 'Refresh necessary') elif k == Coins.LTC: wallet_data['mweb_address'] = w.get('mweb_address', 'Refresh necessary') diff --git a/bin/basicswap_prepare.py b/bin/basicswap_prepare.py index 5631882..60905af 100755 --- a/bin/basicswap_prepare.py +++ b/bin/basicswap_prepare.py @@ -55,6 +55,10 @@ MONERO_VERSION = os.getenv('MONERO_VERSION', '0.18.3.3') MONERO_VERSION_TAG = os.getenv('MONERO_VERSION_TAG', '') XMR_SITE_COMMIT = 'd00169a6decd9470ebdf6a75e3351df4ebcd260a' # Lock hashes.txt to monero version +WOWNERO_VERSION = os.getenv('WOWNERO_VERSION', '0.11.1.0') +WOWNERO_VERSION_TAG = os.getenv('WOWNERO_VERSION_TAG', '') +WOW_SITE_COMMIT = '97e100e1605e9f59bc8ca82a5b237d5562c8a21c' # todo + PIVX_VERSION = os.getenv('PIVX_VERSION', '5.6.1') PIVX_VERSION_TAG = os.getenv('PIVX_VERSION_TAG', '') @@ -85,6 +89,7 @@ known_coins = { 'bitcoin': (BITCOIN_VERSION, BITCOIN_VERSION_TAG, ('laanwj',)), 'namecoin': ('0.18.0', '', ('JeremyRand',)), 'monero': (MONERO_VERSION, MONERO_VERSION_TAG, ('binaryfate',)), + 'wownero': (WOWNERO_VERSION, WOWNERO_VERSION_TAG, ('wowario',)), 'pivx': (PIVX_VERSION, PIVX_VERSION_TAG, ('fuzzbawls',)), 'dash': (DASH_VERSION, DASH_VERSION_TAG, ('pasta',)), 'firo': (FIRO_VERSION, FIRO_VERSION_TAG, ('reuben',)), @@ -103,6 +108,7 @@ expected_key_ids = { 'laanwj': ('1E4AED62986CD25D',), 'JeremyRand': ('2DBE339E29F6294C',), 'binaryfate': ('F0AF4D462A0BDF92',), + 'wowario': ('793504B449C69220',), 'davidburkett38': ('3620E9D387E55666',), 'fuzzbawls': ('C1ABA64407731FD9',), 'pasta': ('52527BEDABE87984',), @@ -157,6 +163,17 @@ XMR_RPC_USER = os.getenv('XMR_RPC_USER', '') XMR_RPC_PWD = os.getenv('XMR_RPC_PWD', '') DEFAULT_XMR_RESTORE_HEIGHT = int(os.getenv('DEFAULT_XMR_RESTORE_HEIGHT', 2245107)) +WOW_RPC_HOST = os.getenv('WOW_RPC_HOST', '127.0.0.1') +BASE_WOW_RPC_PORT = int(os.getenv('BASE_WOW_RPC_PORT', 34598)) +BASE_WOW_ZMQ_PORT = int(os.getenv('BASE_WOW_ZMQ_PORT', 34698)) +BASE_WOW_WALLET_PORT = int(os.getenv('BASE_WOW_WALLET_PORT', 34798)) +WOW_WALLET_RPC_HOST = os.getenv('WOW_WALLET_RPC_HOST', '127.0.0.1') +WOW_WALLET_RPC_USER = os.getenv('WOW_WALLET_RPC_USER', 'wow_wallet_user') +WOW_WALLET_RPC_PWD = os.getenv('WOW_WALLET_RPC_PWD', 'wow_wallet_pwd') +WOW_RPC_USER = os.getenv('WOW_RPC_USER', '') +WOW_RPC_PWD = os.getenv('WOW_RPC_PWD', '') +DEFAULT_WOW_RESTORE_HEIGHT = int(os.getenv('DEFAULT_WOW_RESTORE_HEIGHT', 450000)) + LTC_RPC_HOST = os.getenv('LTC_RPC_HOST', '127.0.0.1') LTC_RPC_PORT = int(os.getenv('LTC_RPC_PORT', 19895)) LTC_ONION_PORT = int(os.getenv('LTC_ONION_PORT', 9333)) @@ -226,12 +243,28 @@ monerod_proxy_config = [ 'hide-my-port=1', # Don't share the p2p port 'p2p-bind-ip=127.0.0.1', # Don't broadcast ip 'in-peers=0', # Changes "error" in log to "incoming connections disabled" + 'out-peers=24', + f'tx-proxy=tor,{TOR_PROXY_HOST}:{TOR_PROXY_PORT},disable_noise,16' # Outgoing tx relay to onion ] monero_wallet_rpc_proxy_config = [ - 'daemon-ssl-allow-any-cert=1', + # 'daemon-ssl-allow-any-cert=1', moved to startup flag ] +wownerod_proxy_config = [ + f'proxy={TOR_PROXY_HOST}:{TOR_PROXY_PORT}', + 'proxy-allow-dns-leaks=0', + 'no-igd=1', # Disable UPnP port mapping + 'hide-my-port=1', # Don't share the p2p port + 'p2p-bind-ip=127.0.0.1', # Don't broadcast ip + 'in-peers=0', # Changes "error" in log to "incoming connections disabled" + 'out-peers=24', + f'tx-proxy=tor,{TOR_PROXY_HOST}:{TOR_PROXY_PORT},disable_noise,16' # Outgoing tx relay to onion +] + +wownero_wallet_rpc_proxy_config = [ + # 'daemon-ssl-allow-any-cert=1', moved to startup flag +] default_socket = socket.socket default_socket_timeout = socket.getdefaulttimeout() @@ -483,9 +516,9 @@ def extractCore(coin, version_data, settings, bin_dir, release_path, extra_opts= logger.info('extractCore %s v%s%s', coin, version, version_tag) extract_core_overwrite = extra_opts.get('extract_core_overwrite', True) - if coin in ('monero', 'firo'): - if coin == 'monero': - bins = ['monerod', 'monero-wallet-rpc'] + if coin in ('monero', 'firo', 'wownero'): + if coin in ('monero', 'wownero'): + bins = [coin + 'd', coin + '-wallet-rpc'] elif coin == 'firo': bins = [coin + 'd', coin + '-cli', coin + '-tx'] else: @@ -624,6 +657,30 @@ def prepareCore(coin, version_data, settings, data_dir, extra_opts={}): assert_path = os.path.join(bin_dir, assert_filename) if not os.path.exists(assert_path): downloadFile(assert_url, assert_path) + elif coin == 'wownero': + use_file_ext = 'tar.bz2' if FILE_EXT == 'tar.gz' else FILE_EXT + release_filename = '{}-{}-{}.{}'.format(coin, version, BIN_ARCH, use_file_ext) + if os_name == 'osx': + os_name = 'mac' + + architecture = 'x64' + release_url = 'https://git.wownero.com/attachments/280753b0-3af0-4a78-a248-8b925e8f4593' + if 'aarch64' in BIN_ARCH: + architecture = 'armv8' + release_url = 'https://git.wownero.com/attachments/0869ffe3-eeff-4240-a185-168ca80fa1e3' + elif 'arm' in BIN_ARCH: + architecture = 'armv7' # 32bit doesn't work + release_url = 'https://git.wownero.com/attachments/ff0c4886-3865-4670-9bc6-63dd60ded0e3' + + release_path = os.path.join(bin_dir, release_filename) + if not os.path.exists(release_path): + downloadFile(release_url, release_path) + + assert_filename = 'wownero-{}-hashes.txt'.format(version) + assert_url = 'https://git.wownero.com/wownero/wownero.org-website/raw/commit/{}/hashes.txt'.format(WOW_SITE_COMMIT) + assert_path = os.path.join(bin_dir, assert_filename) + if not os.path.exists(assert_path): + downloadFile(assert_url, assert_path) elif coin == 'decred': arch_name = BIN_ARCH @@ -642,6 +699,7 @@ def prepareCore(coin, version_data, settings, data_dir, extra_opts={}): release_filename = '{}-{}-{}.{}'.format(coin, version, arch_name, FILE_EXT) release_page_url = 'https://github.com/decred/decred-binaries/releases/download/v{}'.format(version) release_url = release_page_url + '/' + 'decred-{}-v{}.{}'.format(arch_name, version, FILE_EXT) + release_path = os.path.join(bin_dir, release_filename) if not os.path.exists(release_path): downloadFile(release_url, release_path) @@ -789,13 +847,15 @@ def prepareCore(coin, version_data, settings, data_dir, extra_opts={}): pubkeyurls.append('https://raw.githubusercontent.com/dashpay/dash/master/contrib/gitian-keys/pasta.pgp') if coin == 'monero': pubkeyurls.append('https://raw.githubusercontent.com/monero-project/monero/master/utils/gpg_keys/binaryfate.asc') + if coin == 'wownero': + 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 ADD_PUBKEY_URL != '': pubkeyurls.append(ADD_PUBKEY_URL + '/' + pubkey_filename) - if coin in ('monero', 'firo'): + if coin in ('monero', 'wownero', 'firo'): with open(assert_path, 'rb') as fp: verified = gpg.verify_file(fp) @@ -862,7 +922,7 @@ def prepareDataDir(coin, settings, chain, particl_mnemonic, extra_opts={}): if not os.path.exists(data_dir): os.makedirs(data_dir) - if coin == 'monero': + if coin in ('wownero', 'monero'): core_conf_path = os.path.join(data_dir, coin + 'd.conf') if os.path.exists(core_conf_path): exitWithError('{} exists'.format(core_conf_path)) @@ -886,18 +946,28 @@ def prepareDataDir(coin, settings, chain, particl_mnemonic, extra_opts={}): fp.write('zmq-rpc-bind-ip={}\n'.format(COINS_RPCBIND_IP)) fp.write('prune-blockchain=1\n') - if tor_control_password is not None: - for opt_line in monerod_proxy_config: - fp.write(opt_line + '\n') + if coin == 'monero': + if XMR_RPC_USER != '': + fp.write(f'rpc-login={XMR_RPC_USER}:{XMR_RPC_PWD}\n') + if tor_control_password is not None: + for opt_line in monerod_proxy_config: + fp.write(opt_line + '\n') - if XMR_RPC_USER != '': - fp.write(f'rpc-login={XMR_RPC_USER}:{XMR_RPC_PWD}\n') + if coin == 'wownero': + if WOW_RPC_USER != '': + fp.write(f'rpc-login={WOW_RPC_USER}:{WOW_RPC_PWD}\n') + if tor_control_password is not None: + 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.conf') + 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') if os.path.exists(wallet_conf_path): exitWithError('{} exists'.format(wallet_conf_path)) with open(wallet_conf_path, 'w') as fp: @@ -911,9 +981,11 @@ def prepareDataDir(coin, settings, chain, particl_mnemonic, extra_opts={}): fp.write('rpc-bind-ip={}\n'.format(COINS_RPCBIND_IP)) fp.write(f'wallet-dir={config_datadir}\n') fp.write('log-file={}\n'.format(os.path.join(config_datadir, 'wallet.log'))) - fp.write('shared-ringdb-dir={}\n'.format(os.path.join(config_datadir, 'shared-ringdb'))) fp.write('rpc-login={}:{}\n'.format(core_settings['walletrpcuser'], core_settings['walletrpcpassword'])) - + if coin == 'monero': + fp.write('shared-ringdb-dir={}\n'.format(os.path.join(config_datadir, 'shared-ringdb'))) + elif coin == 'wownero': + fp.write('wow-shared-ringdb-dir={}\n'.format(os.path.join(config_datadir, 'shared-ringdb'))) if chain == 'regtest': fp.write('allow-mismatched-daemon-version=1\n') @@ -1087,12 +1159,15 @@ def modify_tor_config(settings, coin, tor_control_password=None, enable=False, e coin_settings = settings['chainclients'][coin] data_dir = coin_settings['datadir'] - if coin == 'monero': + if coin in ('monero', 'wownero'): core_conf_path = os.path.join(data_dir, coin + 'd.conf') 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.conf') + 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') if not os.path.exists(wallet_conf_path): exitWithError('{} does not exist'.format(wallet_conf_path)) @@ -1105,16 +1180,27 @@ def modify_tor_config(settings, coin, tor_control_password=None, enable=False, e # Disable tor first for line in fp_in: skip_line: bool = False - for opt_line in monerod_proxy_config: - setting: str = opt_line[0: opt_line.find('=') + 1] - if line.startswith(setting): - skip_line = True - break + if coin == 'monero': + for opt_line in monerod_proxy_config: + setting: str = opt_line[0: opt_line.find('=') + 1] + if line.startswith(setting): + skip_line = True + break + if coin == 'wownero': + for opt_line in wownerod_proxy_config: + setting: str = opt_line[0: opt_line.find('=') + 1] + if line.startswith(setting): + skip_line = True + break if not skip_line: fp.write(line) if enable: - for opt_line in monerod_proxy_config: - fp.write(opt_line + '\n') + if coin == 'monero': + for opt_line in monerod_proxy_config: + fp.write(opt_line + '\n') + if coin == 'wownero': + for opt_line in wownerod_proxy_config: + fp.write(opt_line + '\n') with open(wallet_conf_path, 'w') as fp: with open(wallet_conf_path + '.last') as fp_in: @@ -1210,6 +1296,7 @@ def printHelp(): print('--htmlhost= Interface to host html server on, default:127.0.0.1.') print('--wshost= Interface to host websocket server on, disable by setting to "none", default\'s to --htmlhost.') print('--xmrrestoreheight=n Block height to restore Monero wallet from, default:{}.'.format(DEFAULT_XMR_RESTORE_HEIGHT)) + print('--wowrestoreheight=n Block height to restore Wownero wallet from, default:{}.'.format(DEFAULT_WOW_RESTORE_HEIGHT)) print('--trustremotenode Set trusted-daemon for XMR, defaults to auto: true when daemon rpchost value is a private ip address else false') print('--noextractover Prevent extracting cores if files exist. Speeds up tests') print('--usetorproxy Use TOR proxy during setup. Note that some download links may be inaccessible over TOR.') @@ -1305,7 +1392,11 @@ def initialise_wallets(particl_wallet_mnemonic, with_coins, data_dir, settings, if c == Coins.XMR: if coin_settings['manage_wallet_daemon']: - filename = 'monero-wallet-rpc' + ('.exe' if os.name == 'nt' else '') + filename = coin_name + '-wallet-rpc' + ('.exe' if os.name == 'nt' else '') + 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 '') daemons.append(startXmrWalletDaemon(coin_settings['datadir'], coin_settings['bindir'], filename)) elif c == Coins.DCR: pass @@ -1470,6 +1561,7 @@ def main(): coins_changed = False htmlhost = '127.0.0.1' xmr_restore_height = DEFAULT_XMR_RESTORE_HEIGHT + wow_restore_height = DEFAULT_WOW_RESTORE_HEIGHT prepare_bin_only = False no_cores = False enable_tor = False @@ -1585,6 +1677,9 @@ def main(): if name == 'xmrrestoreheight': xmr_restore_height = int(s[1]) continue + if name == 'wowrestoreheight': + wow_restore_height = int(s[1]) + continue if name == 'keysdirpath': extra_opts['keysdirpath'] = os.path.expanduser(s[1].strip('"')) continue @@ -1825,6 +1920,28 @@ def main(): 'core_version_group': 18, 'chain_lookups': 'local', 'startup_tries': 40, + }, + 'wownero': { + 'connection_type': 'rpc' if 'wownero' in with_coins else 'none', + 'manage_daemon': True if ('wownero' in with_coins and WOW_RPC_HOST == '127.0.0.1') else False, + 'manage_wallet_daemon': True if ('wownero' in with_coins and WOW_WALLET_RPC_HOST == '127.0.0.1') else False, + 'rpcport': BASE_WOW_RPC_PORT + port_offset, + 'zmqport': BASE_WOW_ZMQ_PORT + port_offset, + 'walletrpcport': BASE_WOW_WALLET_PORT + port_offset, + 'rpchost': WOW_RPC_HOST, + 'trusted_daemon': extra_opts.get('trust_remote_node', 'auto'), + 'walletrpchost': WOW_WALLET_RPC_HOST, + 'walletrpcuser': WOW_WALLET_RPC_USER, + 'walletrpcpassword': WOW_WALLET_RPC_PWD, + 'walletfile': 'swap_wallet', + 'datadir': os.getenv('WOW_DATA_DIR', os.path.join(data_dir, 'wownero')), + 'bindir': os.path.join(bin_dir, 'wownero'), + 'restore_height': wow_restore_height, + 'blocks_confirmed': 2, + 'rpctimeout': 60, + 'walletrpctimeout': 120, + 'walletrpctimeoutlong': 300, + 'core_type_group': 'xmr', } } @@ -1840,6 +1957,9 @@ def main(): if XMR_RPC_USER != '': chainclients['monero']['rpcuser'] = XMR_RPC_USER chainclients['monero']['rpcpassword'] = XMR_RPC_PWD + if WOW_RPC_USER != '': + chainclients['wownero']['rpcuser'] = WOW_RPC_USER + chainclients['wownero']['rpcpassword'] = WOW_RPC_PWD if PIVX_RPC_USER != '': chainclients['pivx']['rpcuser'] = PIVX_RPC_USER chainclients['pivx']['rpcpassword'] = PIVX_RPC_PWD @@ -1854,6 +1974,7 @@ def main(): chainclients['nav']['rpcpassword'] = NAV_RPC_PWD chainclients['monero']['walletsdir'] = os.getenv('XMR_WALLETS_DIR', chainclients['monero']['datadir']) + chainclients['wownero']['walletsdir'] = os.getenv('WOW_WALLETS_DIR', chainclients['wownero']['datadir']) if initwalletsonly: logger.info('Initialising wallets') diff --git a/bin/basicswap_run.py b/bin/basicswap_run.py index 71db102..56937c8 100755 --- a/bin/basicswap_run.py +++ b/bin/basicswap_run.py @@ -98,11 +98,11 @@ def startDaemon(node_dir, bin_dir, daemon_bin, opts=[], extra_config={}): def startXmrDaemon(node_dir, bin_dir, daemon_bin, opts=[]): - daemon_bin = os.path.expanduser(os.path.join(bin_dir, daemon_bin)) + daemon_path = os.path.expanduser(os.path.join(bin_dir, daemon_bin)) datadir_path = os.path.expanduser(node_dir) - args = [daemon_bin, '--non-interactive', '--config-file=' + os.path.join(datadir_path, 'monerod.conf')] + opts - logging.info('Starting node {} --data-dir={}'.format(daemon_bin, node_dir)) + args = [daemon_path, '--non-interactive', '--config-file=' + os.path.join(datadir_path, daemon_bin + '.conf')] + opts + logging.info('Starting node {} --data-dir={}'.format(daemon_path, node_dir)) # 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') @@ -129,7 +129,7 @@ def startXmrWalletDaemon(node_dir, bin_dir, wallet_bin, opts=[]): args += opts if needs_rewrite: - logging.info('Rewriting monero_wallet.conf') + logging.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: @@ -208,10 +208,10 @@ def runClient(fp, data_dir, chain, start_only_coins): except Exception as e: logger.warning('Not starting unknown coin: {}'.format(c)) continue - if c == 'monero': + if c in ('monero', 'wownero'): if v['manage_daemon'] is True: swap_client.log.info(f'Starting {display_name} daemon') - filename = 'monerod' + ('.exe' if os.name == 'nt' else '') + filename = c + '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)) @@ -226,7 +226,7 @@ def runClient(fp, data_dir, chain, start_only_coins): proxy_host, proxy_port = swap_client.getXMRWalletProxy(coin_id, v['rpchost']) if proxy_host: proxy_log_str = ' through proxy' - opts += ['--proxy', f'{proxy_host}:{proxy_port}', ] + opts += ['--proxy', f'{proxy_host}:{proxy_port}', '--daemon-ssl-allow-any-cert', ] swap_client.log.info('daemon-address: {} ({}){}'.format(daemon_addr, 'trusted' if trusted_daemon else 'untrusted', proxy_log_str)) @@ -237,7 +237,7 @@ 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 = 'monero-wallet-rpc' + ('.exe' if os.name == 'nt' else '') + filename = c + '-wallet-rpc' + ('.exe' if os.name == 'nt' else '') daemons.append(startXmrWalletDaemon(v['datadir'], v['bindir'], filename, opts)) pid = daemons[-1].handle.pid swap_client.log.info('Started {} {}'.format(filename, pid)) diff --git a/docker/production/compose-fragments/1_wownero-wallet.yml b/docker/production/compose-fragments/1_wownero-wallet.yml new file mode 100644 index 0000000..11f65cb --- /dev/null +++ b/docker/production/compose-fragments/1_wownero-wallet.yml @@ -0,0 +1,16 @@ + wownero_wallet: + image: i_wownero_wallet + build: + context: wownero_wallet + dockerfile: Dockerfile + container_name: wownero_wallet + volumes: + - ${DATA_PATH}/wownero_wallet:/data + expose: + - ${BASE_WOW_WALLET_PORT} + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + restart: unless-stopped diff --git a/docker/production/compose-fragments/8_wownero-daemon.yml b/docker/production/compose-fragments/8_wownero-daemon.yml new file mode 100644 index 0000000..73fccfe --- /dev/null +++ b/docker/production/compose-fragments/8_wownero-daemon.yml @@ -0,0 +1,16 @@ + wownero_daemon: + image: i_wownero_daemon + build: + context: wownero_daemon + dockerfile: Dockerfile + container_name: wownero_daemon + volumes: + - ${DATA_PATH}/wownero_daemon:/data + expose: + - ${BASE_WOW_RPC_PORT} + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + restart: unless-stopped diff --git a/docker/production/compose-fragments/9_swapprepare.yml b/docker/production/compose-fragments/9_swapprepare.yml index c5a6c37..f6aa102 100644 --- a/docker/production/compose-fragments/9_swapprepare.yml +++ b/docker/production/compose-fragments/9_swapprepare.yml @@ -8,6 +8,8 @@ - ${DATA_PATH}/swapclient:/data/swapclient - ${DATA_PATH}/monero_daemon:/data/monero_daemon - ${DATA_PATH}/monero_wallet:/data/monero_wallet + - ${DATA_PATH}/wownero_daemon:/data/wownero_daemon + - ${DATA_PATH}/wownero_wallet:/data/wownero_wallet - ${DATA_PATH}/particl:/data/particl - ${DATA_PATH}/bitcoin:/data/bitcoin - ${DATA_PATH}/litecoin:/data/litecoin @@ -45,6 +47,16 @@ - XMR_WALLET_RPC_USER - XMR_WALLET_RPC_PWD - DEFAULT_XMR_RESTORE_HEIGHT + - WOW_DATA_DIR + - WOW_RPC_HOST + - BASE_WOW_RPC_PORT + - BASE_WOW_ZMQ_PORT + - WOW_WALLETS_DIR + - WOW_WALLET_RPC_HOST + - BASE_WOW_WALLET_PORT + - WOW_WALLET_RPC_USER + - WOW_WALLET_RPC_PWD + - DEFAULT_WOW_RESTORE_HEIGHT - PIVX_DATA_DIR - PIVX_RPC_HOST - PIVX_RPC_PORT diff --git a/docker/production/example.env b/docker/production/example.env index 0993a4b..44d9fad 100644 --- a/docker/production/example.env +++ b/docker/production/example.env @@ -44,6 +44,16 @@ BASE_XMR_WALLET_PORT=29998 XMR_WALLET_RPC_USER=xmr_wallet_user XMR_WALLET_RPC_PWD=xmr_wallet_pwd +WOW_DATA_DIR=/data/wownero_daemon +WOW_RPC_HOST=wownero_daemon +BASE_WOW_RPC_PORT=34598 + +WOW_WALLETS_DIR=/data/wownero_wallet +WOW_WALLET_RPC_HOST=wownero_wallet +BASE_WOW_WALLET_PORT=34798 +WOW_WALLET_RPC_USER=wow_wallet_user +WOW_WALLET_RPC_PWD=wow_wallet_pwd + PIVX_DATA_DIR=/data/pivx PIVX_RPC_HOST=pivx_core PIVX_RPC_PORT=51473 diff --git a/docker/production/scripts/build_yml_files.py b/docker/production/scripts/build_yml_files.py index 2722f59..aef5e8c 100755 --- a/docker/production/scripts/build_yml_files.py +++ b/docker/production/scripts/build_yml_files.py @@ -57,12 +57,12 @@ def main(): if coin_name == 'particl': # Nothing to do continue - if coin_name == 'monero': - with open(os.path.join(fragments_dir, '1_monero-wallet.yml'), 'rb') as fp_in: + if coin_name in ('monero', 'wownero'): + with open(os.path.join(fragments_dir, '1_{coin_name}-wallet.yml'), 'rb') as fp_in: for line in fp_in: fp.write(line) fpp.write(line) - with open(os.path.join(fragments_dir, '8_monero-daemon.yml'), 'rb') as fp_in: + with open(os.path.join(fragments_dir, '8_{coin_name}-daemon.yml'), 'rb') as fp_in: for line in fp_in: fp.write(line) continue diff --git a/docker/production/wownero_daemon/Dockerfile b/docker/production/wownero_daemon/Dockerfile new file mode 100644 index 0000000..c857e78 --- /dev/null +++ b/docker/production/wownero_daemon/Dockerfile @@ -0,0 +1,24 @@ +FROM i_swapclient as install_stage + +RUN basicswap-prepare --preparebinonly --bindir=/coin_bin --withcoin=wownero --withoutcoins=particl + +FROM debian:bullseye-slim + +COPY --from=install_stage /coin_bin . + +ENV WOWNERO_DATA /data + +RUN groupadd -r wownero && useradd -r -m -g wownero wownero \ + && apt-get update \ + && apt-get install -qq --no-install-recommends gosu \ + && rm -rf /var/lib/apt/lists/* \ + && mkdir -p "$WOWNERO_DATA" \ + && chown -R wownero:wownero "$WOWNERO_DATA" \ + && ln -sfn "$WOWNERO_DATA" /home/wownero/.wownero \ + && chown -h wownero:wownero /home/wownero/.wownero +VOLUME $WOWNERO_DATA + +COPY entrypoint.sh /entrypoint.sh +ENTRYPOINT ["/entrypoint.sh"] + +CMD ["/wownero/wownerod", "--non-interactive", "--config-file=/home/wownero/.wownero/wownerod.conf", "--confirm-external-bind"] diff --git a/docker/production/wownero_daemon/entrypoint.sh b/docker/production/wownero_daemon/entrypoint.sh new file mode 100755 index 0000000..6048a57 --- /dev/null +++ b/docker/production/wownero_daemon/entrypoint.sh @@ -0,0 +1,11 @@ +#!/bin/bash +set -e + +if [[ "$1" == "wownerod" ]]; then + mkdir -p "$WOWNERO_DATA" + + chown -h wownero:wownero /home/wownero/.wownero + exec gosu wownero "$@" +else + exec "$@" +fi diff --git a/docker/production/wownero_wallet/Dockerfile b/docker/production/wownero_wallet/Dockerfile new file mode 100644 index 0000000..5a6d398 --- /dev/null +++ b/docker/production/wownero_wallet/Dockerfile @@ -0,0 +1,16 @@ +FROM i_wownero_daemon + +ENV WOWNERO_DATA /data + +RUN groupadd -r wownero_wallet && useradd -r -m -g wownero_wallet wownero_wallet \ + && apt-get update \ + && apt-get install -qq --no-install-recommends gosu \ + && rm -rf /var/lib/apt/lists/* \ + && mkdir -p "$WOWNERO_DATA" \ + && chown -R wownero_wallet:wownero_wallet "$WOWNERO_DATA" +VOLUME $WOWNERO_DATA + +COPY entrypoint.sh /entrypoint.sh +ENTRYPOINT ["/entrypoint.sh"] + +CMD ["/wownero/wownero-wallet-rpc", "--non-interactive", "--config-file=/data/wownero-wallet-rpc.conf", "--confirm-external-bind"] diff --git a/docker/production/wownero_wallet/entrypoint.sh b/docker/production/wownero_wallet/entrypoint.sh new file mode 100755 index 0000000..a0ed9fe --- /dev/null +++ b/docker/production/wownero_wallet/entrypoint.sh @@ -0,0 +1,11 @@ +#!/bin/bash +set -e + +if [[ "$1" == "wownero-wallet-rpc" ]]; then + mkdir -p "$WOWNERO_DATA" + + chown -h wownero_wallet:wownero_wallet /data + exec gosu wownero_wallet "$@" +else + exec "$@" +fi diff --git a/setup.py b/setup.py index 1733dfc..867105c 100644 --- a/setup.py +++ b/setup.py @@ -29,6 +29,7 @@ setuptools.setup( "particl", "bitcoin", "monero", + "wownero", ], install_requires=[ "wheel", diff --git a/tox.ini b/tox.ini index f99fc0c..7a100c7 100644 --- a/tox.ini +++ b/tox.ini @@ -11,6 +11,7 @@ passenv = BITCOIN_BINDIR LITECOIN_BINDIR XMR_BINDIR + WOW_BINDIR TEST_PREPARE_PATH TEST_RELOAD_PATH deps =