diff --git a/basicswap/basicswap.py b/basicswap/basicswap.py index 2e4901f..1d517c5 100644 --- a/basicswap/basicswap.py +++ b/basicswap/basicswap.py @@ -656,7 +656,11 @@ class BasicSwap(BaseApp): self.log.error('Sanity checks failed: %s', str(e)) elif c == Coins.XMR: - ci.ensureWalletExists() + try: + ci.ensureWalletExists() + except Exception as e: + self.log.warning('Can\'t open XMR wallet, could be locked.') + continue self.checkWalletSeed(c) @@ -740,7 +744,7 @@ class BasicSwap(BaseApp): yield c def changeWalletPasswords(self, old_password, new_password): - + # Only the main wallet password is changed for monero, avoid issues by preventing until active swaps are complete if len(self.swaps_in_progress) > 0: raise ValueError('Can\'t change passwords while swaps are in progress') @@ -767,6 +771,7 @@ class BasicSwap(BaseApp): if coin_type == Coins.PART: return ci = self.ci(coin_type) + db_key_coin_name = ci.coin_name().lower() self.log.info('Initialising {} wallet.'.format(ci.coin_name())) if coin_type == Coins.XMR: @@ -775,7 +780,7 @@ class BasicSwap(BaseApp): ci.initialiseWallet(key_view, key_spend) root_address = ci.getAddressFromKeys(key_view, key_spend) - key_str = 'main_wallet_addr_' + ci.coin_name().lower() + key_str = 'main_wallet_addr_' + db_key_coin_name self.setStringKV(key_str, root_address) return @@ -789,9 +794,23 @@ class BasicSwap(BaseApp): self.log.error('initialiseWallet failed: {}'.format(str(e))) if raise_errors: raise e + return - key_str = 'main_wallet_seedid_' + ci.coin_name().lower() - self.setStringKV(key_str, root_hash.hex()) + try: + session = self.openSession() + key_str = 'main_wallet_seedid_' + db_key_coin_name + self.setStringKV(key_str, root_hash.hex(), session) + + # Clear any saved addresses + self.clearStringKV('receive_addr_' + db_key_coin_name, session) + self.clearStringKV('stealth_addr_' + db_key_coin_name, session) + + coin_id = int(coin_type) + info_type = 1 # wallet + query_str = f'DELETE FROM wallets WHERE coin_id = {coin_id} AND balance_type = {info_type}' + session.execute(query_str) + finally: + self.closeSession(session) def updateIdentityBidState(self, session, address, bid): identity_stats = session.query(KnownIdentity).filter_by(address=address).first() @@ -831,20 +850,18 @@ class BasicSwap(BaseApp): session.remove() self.mxDB.release() - def setStringKV(self, str_key, str_val): - with self.mxDB: - try: - session = scoped_session(self.session_factory) - kv = session.query(DBKVString).filter_by(key=str_key).first() - if not kv: - kv = DBKVString(key=str_key, value=str_val) - else: - kv.value = str_val - session.add(kv) - session.commit() - finally: - session.close() - session.remove() + def setStringKV(self, str_key, str_val, session=None): + try: + use_session = self.openSession(session) + kv = use_session.query(DBKVString).filter_by(key=str_key).first() + if not kv: + kv = DBKVString(key=str_key, value=str_val) + else: + kv.value = str_val + use_session.add(kv) + finally: + if session is None: + self.closeSession(use_session) def getStringKV(self, str_key): self.mxDB.acquire() @@ -859,6 +876,16 @@ class BasicSwap(BaseApp): session.remove() self.mxDB.release() + def clearStringKV(self, str_key, str_val): + with self.mxDB: + try: + session = scoped_session(self.session_factory) + session.execute('DELETE FROM kv_string WHERE key = "{}" '.format(str_key)) + session.commit() + finally: + session.close() + session.remove() + def activateBid(self, session, bid): if bid.bid_id in self.swaps_in_progress: self.log.debug('Bid %s is already in progress', bid.bid_id.hex()) @@ -1442,6 +1469,7 @@ class BasicSwap(BaseApp): record.bid_id = bid_id record.tx_type = tx_type addr = record.addr + ensure(self.ci(coin_type).isAddressMine(addr), 'Pool address not owned by wallet!') session.add(record) session.commit() finally: @@ -1539,6 +1567,7 @@ class BasicSwap(BaseApp): if expect_address is None: self.log.warning('Can\'t find expected main wallet address for coin {}'.format(ci.coin_name())) return False + ci._have_checked_seed = True if expect_address == ci.getMainWalletAddress(): ci.setWalletSeedWarning(False) return True diff --git a/basicswap/interface/btc.py b/basicswap/interface/btc.py index 0277bd2..89063e0 100644 --- a/basicswap/interface/btc.py +++ b/basicswap/interface/btc.py @@ -188,6 +188,7 @@ class BTCInterface(CoinInterface): self._connection_type = coin_settings['connection_type'] self._sc = swap_client self._log = self._sc.log if self._sc and self._sc.log else logging + self._expect_seedid_hex = None def using_segwit(self): return self._use_segwit @@ -297,6 +298,7 @@ class BTCInterface(CoinInterface): return self.rpc_callback('getwalletinfo')['hdseedid'] def checkExpectedSeed(self, expect_seedid): + self._expect_seedid_hex = expect_seedid return expect_seedid == self.getWalletSeedID() def getNewAddress(self, use_segwit, label='swap_receive'): @@ -305,6 +307,15 @@ class BTCInterface(CoinInterface): args.append('bech32') return self.rpc_callback('getnewaddress', args) + def isAddressMine(self, address): + addr_info = self.rpc_callback('getaddressinfo', [address]) + return addr_info['ismine'] + + def checkAddressMine(self, address): + addr_info = self.rpc_callback('getaddressinfo', [address]) + ensure(addr_info['ismine'], 'ismine is false') + ensure(addr_info['hdseedid'] == self._expect_seedid_hex, 'unexpected seedid') + def get_fee_rate(self, conf_target=2): try: fee_rate = self.rpc_callback('estimatesmartfee', [conf_target])['feerate'] diff --git a/basicswap/interface/firo.py b/basicswap/interface/firo.py index 0738853..20a3aec 100644 --- a/basicswap/interface/firo.py +++ b/basicswap/interface/firo.py @@ -55,6 +55,10 @@ class FIROInterface(BTCInterface): addr_info = self.rpc_callback('validateaddress', [address]) return addr_info['iswatchonly'] + def isAddressMine(self, address): + addr_info = self.rpc_callback('validateaddress', [address]) + return addr_info['ismine'] + def getSCLockScriptAddress(self, lock_script): lock_tx_dest = self.getScriptDest(lock_script) address = self.encodeScriptDest(lock_tx_dest) diff --git a/basicswap/interface/xmr.py b/basicswap/interface/xmr.py index 38e41d7..f472052 100644 --- a/basicswap/interface/xmr.py +++ b/basicswap/interface/xmr.py @@ -75,6 +75,7 @@ class XMRInterface(CoinInterface): 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 def setFeePriority(self, new_priority): ensure(new_priority >= 0 and new_priority < 4, 'Invalid fee_priority value') @@ -534,6 +535,13 @@ class XMRInterface(CoinInterface): 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): self._log.info('lockWallet - {}'.format(self.ticker())) self._wallet_password = None + + def isAddressMine(self, address): + # TODO + return True diff --git a/basicswap/js_server.py b/basicswap/js_server.py index 0285241..892a56b 100644 --- a/basicswap/js_server.py +++ b/basicswap/js_server.py @@ -32,6 +32,7 @@ from .ui.util import ( have_data_entry, tickerToCoinId, listOldBidStates, + checkAddressesOwned, ) from .ui.page_offers import postNewOffer from .protocols.xmr_swap_1 import recoverNoScriptTxnWithKey, getChainBSplitKey @@ -117,8 +118,10 @@ def js_wallets(self, url_split, post_string, is_json): rv = swap_client.getWalletInfo(coin_type) rv.update(swap_client.getBlockchainInfo(coin_type)) + ci = swap_client.ci(coin_type) + checkAddressesOwned(ci, rv) return bytes(json.dumps(rv), 'UTF-8') - return bytes(json.dumps(self.server.swap_client.getWalletsInfo({'ticker_key': True})), 'UTF-8') + return bytes(json.dumps(swap_client.getWalletsInfo({'ticker_key': True})), 'UTF-8') def js_offers(self, url_split, post_string, is_json, sent=False): diff --git a/basicswap/templates/wallet.html b/basicswap/templates/wallet.html index 9c6086d..72f494f 100644 --- a/basicswap/templates/wallet.html +++ b/basicswap/templates/wallet.html @@ -170,12 +170,18 @@ Bootstrapping: {{ w.bootstrapping }} - + + {% endif %} + {% if w.encrypted %} + + Locked: + {{ w.locked }} + {% endif %} Expected Seed: {{ w.expected_seed }} - {% if w.expected_seed != true %} + {% if w.expected_seed != true %} {# Only show addresses if wallet seed is correct #} @@ -411,4 +417,4 @@ {% include 'footer.html' %} - \ No newline at end of file + diff --git a/basicswap/templates/wallets.html b/basicswap/templates/wallets.html index a10857b..6aecb91 100644 --- a/basicswap/templates/wallets.html +++ b/basicswap/templates/wallets.html @@ -82,7 +82,9 @@

Last Updated:

{{ w.lastupdated }}
{% if w.bootstrapping %}
-

Bootstrapping:

{{ w.bootstrapping }}
{% endif %} +

Bootstrapping:

{{ w.bootstrapping }}{% endif %}{% if w.encrypted %} +
+

Locked:

{{ w.locked }}
{% endif %}

Expected Seed:

{{ w.expected_seed }}
Blockchain {{ w.synced }}%
@@ -109,4 +111,4 @@ {% include 'footer.html' %} - \ No newline at end of file + diff --git a/basicswap/ui/page_wallet.py b/basicswap/ui/page_wallet.py index eec23a8..9093a91 100644 --- a/basicswap/ui/page_wallet.py +++ b/basicswap/ui/page_wallet.py @@ -9,6 +9,7 @@ import traceback from .util import ( get_data_entry, have_data_entry, + checkAddressesOwned, ) from basicswap.util import ( ensure, @@ -31,6 +32,8 @@ def format_wallet_data(ci, w): 'blocks': w.get('blocks', '?'), 'synced': w.get('synced', '?'), 'expected_seed': w.get('expected_seed', '?'), + 'encrypted': w.get('encrypted', '?'), + 'locked': w.get('locked', '?'), 'updating': w.get('updating', '?'), 'havedata': True, } @@ -55,6 +58,8 @@ def format_wallet_data(ci, w): wf['anon_balance'] = w.get('anon_balance', '?') if 'anon_pending' in w and float(w['anon_pending']) > 0.0: wf['anon_pending'] = w['anon_pending'] + + checkAddressesOwned(ci, wf) return wf @@ -297,7 +302,6 @@ def page_wallet(self, url_split, post_string): if show_utxo_groups: utxo_groups = '' - unspent_by_addr = ci.getUnspentsByAddr() sorted_unspent_by_addr = sorted(unspent_by_addr.items(), key=lambda x: x[1], reverse=True) @@ -307,6 +311,8 @@ def page_wallet(self, url_split, post_string): wallet_data['show_utxo_groups'] = True wallet_data['utxo_groups'] = utxo_groups + checkAddressesOwned(ci, wallet_data) + template = server.env.get_template('wallet.html') return self.render_template(template, { 'messages': messages, diff --git a/basicswap/ui/util.py b/basicswap/ui/util.py index 11ceb14..a4131c2 100644 --- a/basicswap/ui/util.py +++ b/basicswap/ui/util.py @@ -423,3 +423,17 @@ def listAvailableCoins(swap_client, with_variants=True, split_from=False): if split_from: return coins_from, coins return coins + + +def checkAddressesOwned(ci, wallet_info): + if 'stealth_address' in wallet_info: + if wallet_info['stealth_address'] != '?' and \ + not ci.isAddressMine(wallet_info['stealth_address']): + ci._log.error('Unowned stealth address: {}'.format(wallet_info['stealth_address'])) + wallet_info['stealth_address'] = 'Error: unowned address' + + if 'deposit_address' in wallet_info: + if wallet_info['deposit_address'] != 'Refresh necessary' and \ + not ci.isAddressMine(wallet_info['deposit_address']): + ci._log.error('Unowned deposit address: {}'.format(wallet_info['deposit_address'])) + wallet_info['deposit_address'] = 'Error: unowned address' diff --git a/bin/basicswap_prepare.py b/bin/basicswap_prepare.py index 58da801..345a011 100755 --- a/bin/basicswap_prepare.py +++ b/bin/basicswap_prepare.py @@ -160,8 +160,8 @@ TEST_ONION_LINK = toBool(os.getenv('TEST_ONION_LINK', 'false')) BITCOIN_FASTSYNC_URL = os.getenv('BITCOIN_FASTSYNC_URL', 'http://utxosets.blob.core.windows.net/public/') BITCOIN_FASTSYNC_FILE = os.getenv('BITCOIN_FASTSYNC_FILE', 'utxo-snapshot-bitcoin-mainnet-720179.tar') -# Set to false when running individual docker containers -START_DAEMONS = toBool(os.getenv('START_DAEMONS', 'true')) +# Encrypt new wallets with this password, must match the Particl wallet password when adding coins +WALLET_ENCRYPTION_PWD = os.getenv('WALLET_ENCRYPTION_PWD', '') use_tor_proxy = False @@ -933,14 +933,49 @@ def finalise_daemon(d): fp.close() +def test_particl_encryption(data_dir, settings, chain, use_tor_proxy): + swap_client = None + daemons = [] + daemon_args = ['-noconnect', '-nodnsseed', '-nofindpeers', '-nostaking'] + if not use_tor_proxy: + # Cannot set -bind or -whitebind together with -listen=0 + daemon_args.append('-nolisten') + with open(os.path.join(data_dir, 'basicswap.log'), 'a') as fp: + try: + swap_client = BasicSwap(fp, data_dir, settings, chain) + c = Coins.PART + coin_name = 'particl' + coin_settings = settings['chainclients'][coin_name] + if coin_settings['manage_daemon']: + filename = coin_name + 'd' + ('.exe' if os.name == 'nt' else '') + daemons.append(startDaemon(coin_settings['datadir'], coin_settings['bindir'], filename, daemon_args)) + swap_client.setDaemonPID(c, daemons[-1].pid) + swap_client.setCoinRunParams(c) + swap_client.createCoinInterface(c) + + if swap_client.ci(c).isWalletEncrypted(): + logger.info('Particl Wallet is encrypted') + if WALLET_ENCRYPTION_PWD == '': + raise ValueError('Must set WALLET_ENCRYPTION_PWD to add coin when Particl wallet is encrypted') + swap_client.ci(c).unlockWallet(WALLET_ENCRYPTION_PWD) + finally: + if swap_client: + swap_client.finalise() + del swap_client + for d in daemons: + finalise_daemon(d) + + def initialise_wallets(particl_wallet_mnemonic, with_coins, data_dir, settings, chain, use_tor_proxy): + swap_client = None daemons = [] daemon_args = ['-noconnect', '-nodnsseed'] if not use_tor_proxy: # Cannot set -bind or -whitebind together with -listen=0 daemon_args.append('-nolisten') - try: - with open(os.path.join(data_dir, 'basicswap.log'), 'a') as fp: + + with open(os.path.join(data_dir, 'basicswap.log'), 'a') as fp: + try: swap_client = BasicSwap(fp, data_dir, settings, chain) # Always start Particl, it must be running to initialise a wallet in addcoin mode @@ -950,20 +985,19 @@ def initialise_wallets(particl_wallet_mnemonic, with_coins, data_dir, settings, coin_settings = settings['chainclients'][coin_name] c = swap_client.getCoinIdFromName(coin_name) - if START_DAEMONS: - if c == Coins.XMR: - if coin_settings['manage_wallet_daemon']: - daemons.append(startXmrWalletDaemon(coin_settings['datadir'], coin_settings['bindir'], 'monero-wallet-rpc')) - else: - if coin_settings['manage_daemon']: - filename = coin_name + 'd' + ('.exe' if os.name == 'nt' else '') - coin_args = ['-nofindpeers', '-nostaking'] if c == Coins.PART else [] + if c == Coins.XMR: + if coin_settings['manage_wallet_daemon']: + daemons.append(startXmrWalletDaemon(coin_settings['datadir'], coin_settings['bindir'], 'monero-wallet-rpc')) + else: + if coin_settings['manage_daemon']: + filename = coin_name + 'd' + ('.exe' if os.name == 'nt' else '') + coin_args = ['-nofindpeers', '-nostaking'] if c == Coins.PART else [] - if c == Coins.FIRO: - coin_args += ['-hdseed={}'.format(swap_client.getWalletKey(Coins.FIRO, 1).hex())] + if c == Coins.FIRO: + coin_args += ['-hdseed={}'.format(swap_client.getWalletKey(Coins.FIRO, 1).hex())] - daemons.append(startDaemon(coin_settings['datadir'], coin_settings['bindir'], filename, daemon_args + coin_args)) - swap_client.setDaemonPID(c, daemons[-1].pid) + daemons.append(startDaemon(coin_settings['datadir'], coin_settings['bindir'], filename, daemon_args + coin_args)) + swap_client.setDaemonPID(c, daemons[-1].pid) swap_client.setCoinRunParams(c) swap_client.createCoinInterface(c) @@ -975,16 +1009,22 @@ def initialise_wallets(particl_wallet_mnemonic, with_coins, data_dir, settings, logger.info('Creating wallet.dat for {}.'.format(getCoinName(c))) if c == Coins.BTC: - # wallet_name, wallet_name, blank, passphrase, avoid_reuse, descriptors + # wallet_name, disable_private_keys, blank, passphrase, avoid_reuse, descriptors swap_client.callcoinrpc(c, 'createwallet', ['wallet.dat', False, True, '', False, False]) else: swap_client.callcoinrpc(c, 'createwallet', ['wallet.dat']) - if 'particl' in with_coins and c == Coins.PART: - logger.info('Loading Particl mnemonic') - if particl_wallet_mnemonic is None: - particl_wallet_mnemonic = swap_client.callcoinrpc(Coins.PART, 'mnemonic', ['new'])['mnemonic'] - swap_client.callcoinrpc(Coins.PART, 'extkeyimportmaster', [particl_wallet_mnemonic]) + if c == Coins.PART: + if 'particl' in with_coins: + logger.info('Loading Particl mnemonic') + if particl_wallet_mnemonic is None: + particl_wallet_mnemonic = swap_client.callcoinrpc(Coins.PART, 'mnemonic', ['new'])['mnemonic'] + swap_client.callcoinrpc(Coins.PART, 'extkeyimportmaster', [particl_wallet_mnemonic]) + if WALLET_ENCRYPTION_PWD != '': + swap_client.ci(c).changeWalletPassword('', WALLET_ENCRYPTION_PWD) + # Particl wallet must be unlocked to call getWalletKey + if WALLET_ENCRYPTION_PWD != '': + swap_client.ci(c).unlockWallet(WALLET_ENCRYPTION_PWD) for coin_name in with_coins: c = swap_client.getCoinIdFromName(coin_name) @@ -992,12 +1032,15 @@ def initialise_wallets(particl_wallet_mnemonic, with_coins, data_dir, settings, continue swap_client.waitForDaemonRPC(c) swap_client.initialiseWallet(c) + if WALLET_ENCRYPTION_PWD != '': + swap_client.ci(c).changeWalletPassword('', WALLET_ENCRYPTION_PWD) - swap_client.finalise() - del swap_client - finally: - for d in daemons: - finalise_daemon(d) + finally: + if swap_client: + swap_client.finalise() + del swap_client + for d in daemons: + finalise_daemon(d) if particl_wallet_mnemonic is not None: if particl_wallet_mnemonic: @@ -1369,6 +1412,9 @@ def main(): logger.info('Adding coin: %s', add_coin) settings = load_config(config_path) + # Ensure Particl wallet is unencrypted or correct password is supplied + test_particl_encryption(data_dir, settings, chain, use_tor_proxy) + if add_coin in settings['chainclients']: coin_settings = settings['chainclients'][add_coin] if coin_settings['connection_type'] == 'none' and coin_settings['manage_daemon'] is False: diff --git a/bin/basicswap_run.py b/bin/basicswap_run.py index df4b795..8288fda 100755 --- a/bin/basicswap_run.py +++ b/bin/basicswap_run.py @@ -115,9 +115,15 @@ def ws_message_received(client, server, message): def runClient(fp, data_dir, chain): global swap_client + daemons = [] + pids = [] + threads = [] settings_path = os.path.join(data_dir, cfg.CONFIG_FILENAME) pids_path = os.path.join(data_dir, '.pids') + if os.getenv('WALLET_ENCRYPTION_PWD', '') != '': + raise ValueError('Please unset the WALLET_ENCRYPTION_PWD environment variable.') + if not os.path.exists(settings_path): raise ValueError('Settings file not found: ' + str(settings_path)) @@ -126,10 +132,6 @@ def runClient(fp, data_dir, chain): swap_client = BasicSwap(fp, data_dir, settings, chain) - daemons = [] - pids = [] - threads = [] - if os.path.exists(pids_path): with open(pids_path) as fd: for ln in fd: diff --git a/doc/tor.md b/doc/tor.md index d9a50d8..bbba3c8 100644 --- a/doc/tor.md +++ b/doc/tor.md @@ -15,6 +15,7 @@ basicswap-prepare can be configured to download all binaries through tor and to Docker will create directories instead of files if these don't exist. + mkdir -p $COINDATA_PATH/tor touch $COINDATA_PATH/tor/torrc