From 91e285bf4af948617e9541f539381ee9f3e17c75 Mon Sep 17 00:00:00 2001 From: tecnovert Date: Wed, 6 Jul 2022 00:46:37 +0200 Subject: [PATCH] ui: Split wallet cached data into balance and blockchain state. Add XMR synced indicator. --- basicswap/basicswap.py | 122 ++++++++------ basicswap/db.py | 2 - basicswap/db_upgrades.py | 4 +- basicswap/http_server.py | 304 +-------------------------------- basicswap/interface_xmr.py | 38 +++-- basicswap/js_server.py | 7 +- basicswap/ui/page_wallet.py | 327 ++++++++++++++++++++++++++++++++++++ 7 files changed, 436 insertions(+), 368 deletions(-) create mode 100644 basicswap/ui/page_wallet.py diff --git a/basicswap/basicswap.py b/basicswap/basicswap.py index 513086e..0e04956 100644 --- a/basicswap/basicswap.py +++ b/basicswap/basicswap.py @@ -5258,60 +5258,82 @@ class BasicSwap(BaseApp): } return rv - def getWalletInfo(self, coin): - + def getBlockchainInfo(self, coin): ci = self.ci(coin) - blockchaininfo = ci.getBlockchainInfo() - walletinfo = ci.getWalletInfo() - scale = chainparams[coin]['decimal_places'] - rv = { - 'version': self.coin_clients[coin]['core_version'], - 'deposit_address': self.getCachedAddressForCoin(coin), - 'name': ci.coin_name(), - 'blocks': blockchaininfo['blocks'], - 'balance': format_amount(make_int(walletinfo['balance'], scale), scale), - 'unconfirmed': format_amount(make_int(walletinfo.get('unconfirmed_balance'), scale), scale), - 'synced': '{0:.2f}'.format(round(blockchaininfo['verificationprogress'], 2)), - 'expected_seed': ci.knownWalletSeed(), - } + try: + blockchaininfo = ci.getBlockchainInfo() - if coin == Coins.PART: - rv['stealth_address'] = self.getCachedStealthAddressForCoin(Coins.PART) - rv['anon_balance'] = walletinfo['anon_balance'] - 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: - rv['main_address'] = self.getCachedMainWalletAddress(ci) + rv = { + 'version': self.coin_clients[coin]['core_version'], + 'name': ci.coin_name(), + 'blocks': blockchaininfo['blocks'], + 'synced': '{0:.2f}'.format(round(blockchaininfo['verificationprogress'], 2)), + } - return rv + return rv + except Exception as e: + self.log.warning('getWalletInfo failed with: %s', str(e)) - def updateWalletInfo(self, coin): - wi = self.getWalletInfo(coin) + def getWalletInfo(self, coin): + ci = self.ci(coin) - # Store wallet info to db so it's available after startup + try: + walletinfo = ci.getWalletInfo() + scale = chainparams[coin]['decimal_places'] + rv = { + 'deposit_address': self.getCachedAddressForCoin(coin), + 'balance': format_amount(make_int(walletinfo['balance'], scale), scale), + 'unconfirmed': format_amount(make_int(walletinfo.get('unconfirmed_balance'), scale), scale), + 'expected_seed': ci.knownWalletSeed(), + } + + if coin == Coins.PART: + rv['stealth_address'] = self.getCachedStealthAddressForCoin(Coins.PART) + rv['anon_balance'] = walletinfo['anon_balance'] + 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: + rv['main_address'] = self.getCachedMainWalletAddress(ci) + + return rv + except Exception as e: + self.log.warning('getWalletInfo failed with: %s', str(e)) + + def addWalletInfoRecord(self, coin, info_type, wi): + coin_id = int(coin) self.mxDB.acquire() try: - rv = [] now = int(time.time()) session = scoped_session(self.session_factory) - - session.add(Wallets(coin_id=coin, wallet_data=json.dumps(wi), created_at=now)) - - coin_id = int(coin) - query_str = f'DELETE FROM wallets WHERE coin_id = {coin_id} AND record_id NOT IN (SELECT record_id FROM wallets WHERE coin_id = {coin_id} ORDER BY created_at DESC LIMIT 3 )' + session.add(Wallets(coin_id=coin, balance_type=info_type, wallet_data=json.dumps(wi), created_at=now)) + query_str = f'DELETE FROM wallets WHERE (coin_id = {coin_id} AND balance_type = {info_type}) AND record_id NOT IN (SELECT record_id FROM wallets WHERE coin_id = {coin_id} AND balance_type = {info_type} ORDER BY created_at DESC LIMIT 3 )' session.execute(query_str) session.commit() except Exception as e: - self.log.error(f'updateWalletInfo {e}') - + self.log.error(f'addWalletInfoRecord {e}') finally: session.close() session.remove() - self._updating_wallets_info[int(coin)] = False self.mxDB.release() + def updateWalletInfo(self, coin): + # Store wallet info to db so it's available after startup + try: + bi = self.getBlockchainInfo(coin) + if bi: + self.addWalletInfoRecord(coin, 0, bi) + + # monero-wallet-rpc is slow/unresponsive while syncing + wi = self.getWalletInfo(coin) + if wi: + self.addWalletInfoRecord(coin, 1, wi) + except Exception as e: + self.log.error(f'updateWalletInfo {e}') + finally: + self._updating_wallets_info[int(coin)] = False + def updateWalletsInfo(self, force_update=False, only_coin=None): now = int(time.time()) if not force_update and now - self._last_updated_wallets_info < 30: @@ -5335,6 +5357,7 @@ class BasicSwap(BaseApp): if self.coin_clients[c]['connection_type'] == 'rpc': try: rv[c] = self.getWalletInfo(c) + rv[c].update(self.getBlockchainInfo(c)) except Exception as ex: rv[c] = {'name': chainparams[c]['name'].capitalize(), 'error': str(ex)} return rv @@ -5347,8 +5370,8 @@ class BasicSwap(BaseApp): where_str = '' if opts is not None and 'coin_id' in opts: where_str = 'WHERE coin_id = {}'.format(opts['coin_id']) - inner_str = f'SELECT coin_id, MAX(created_at) as max_created_at FROM wallets {where_str} GROUP BY coin_id' - query_str = 'SELECT a.coin_id, wallet_data, created_at FROM wallets a, ({}) b WHERE a.coin_id = b.coin_id AND a.created_at = b.max_created_at'.format(inner_str) + inner_str = f'SELECT coin_id, balance_type, MAX(created_at) as max_created_at FROM wallets {where_str} GROUP BY coin_id, balance_type' + query_str = 'SELECT a.coin_id, a.balance_type, wallet_data, created_at FROM wallets a, ({}) b WHERE a.coin_id = b.coin_id AND a.balance_type = b.balance_type AND a.created_at = b.max_created_at'.format(inner_str) q = session.execute(query_str) for row in q: @@ -5358,16 +5381,20 @@ class BasicSwap(BaseApp): # Skip cached info if coin was disabled continue - wallet_data = json.loads(row[1]) - wallet_data['lastupdated'] = row[2] - wallet_data['updating'] = self._updating_wallets_info.get(coin_id, False) + wallet_data = json.loads(row[2]) + if row[1] == 1: + wallet_data['lastupdated'] = row[3] + wallet_data['updating'] = self._updating_wallets_info.get(coin_id, False) - # Ensure the latest deposit address is displayed - q = session.execute('SELECT value FROM kv_string WHERE key = "receive_addr_{}"'.format(chainparams[coin_id]['name'])) - for row in q: - wallet_data['deposit_address'] = row[0] + # Ensure the latest deposit address is displayed + q = session.execute('SELECT value FROM kv_string WHERE key = "receive_addr_{}"'.format(chainparams[coin_id]['name'])) + for row in q: + wallet_data['deposit_address'] = row[0] - rv[coin_id] = wallet_data + if coin_id in rv: + rv[coin_id].update(wallet_data) + else: + rv[coin_id] = wallet_data finally: session.close() session.remove() @@ -5383,6 +5410,7 @@ class BasicSwap(BaseApp): if coin_id not in rv: rv[coin_id] = { 'name': chainparams[c]['name'].capitalize(), + 'no_data': True, 'updating': self._updating_wallets_info.get(coin_id, False), } @@ -5602,7 +5630,6 @@ class BasicSwap(BaseApp): def getAutomationStrategy(self, strategy_id): self.mxDB.acquire() try: - rv = [] session = scoped_session(self.session_factory) return session.query(AutomationStrategy).filter_by(record_id=strategy_id).first() finally: @@ -5613,7 +5640,6 @@ class BasicSwap(BaseApp): def getLinkedStrategy(self, linked_type, linked_id): self.mxDB.acquire() try: - rv = [] session = scoped_session(self.session_factory) query_str = 'SELECT links.strategy_id, strats.label FROM automationlinks links' + \ ' LEFT JOIN automationstrategies strats ON strats.record_id = links.strategy_id' + \ diff --git a/basicswap/db.py b/basicswap/db.py index e5bf6f7..f0ce860 100644 --- a/basicswap/db.py +++ b/basicswap/db.py @@ -391,8 +391,6 @@ class Wallets(Base): wallet_name = sa.Column(sa.String) wallet_data = sa.Column(sa.String) balance_type = sa.Column(sa.Integer) - amount = sa.Column(sa.BigInteger) - updated_at = sa.Column(sa.BigInteger) created_at = sa.Column(sa.BigInteger) diff --git a/basicswap/db_upgrades.py b/basicswap/db_upgrades.py index 6e1784f..18e16ba 100644 --- a/basicswap/db_upgrades.py +++ b/basicswap/db_upgrades.py @@ -102,9 +102,8 @@ def upgradeDatabase(self, db_version): record_id INTEGER NOT NULL, coin_id INTEGER, wallet_name VARCHAR, + wallet_data VARCHAR, balance_type INTEGER, - amount BIGINT, - updated_at BIGINT, created_at BIGINT, PRIMARY KEY (record_id))''') db_version += 1 @@ -216,6 +215,7 @@ def upgradeDatabase(self, db_version): db_version += 1 session.execute('ALTER TABLE xmr_swaps ADD COLUMN coin_a_lock_release_msg_id BLOB') session.execute('ALTER TABLE xmr_swaps RENAME COLUMN coin_a_lock_refund_spend_tx_msg_id TO coin_a_lock_spend_tx_msg_id') + if current_version != db_version: self.db_version = db_version self.setIntKVInSession('db_version', db_version, session) diff --git a/basicswap/http_server.py b/basicswap/http_server.py index a53a9a0..500183d 100644 --- a/basicswap/http_server.py +++ b/basicswap/http_server.py @@ -23,7 +23,6 @@ from .util import ( from .chainparams import ( Coins, chainparams, - getCoinIdFromTicker, ) from .basicswap_util import ( BidStates, @@ -61,6 +60,7 @@ from .ui.util import ( ) from .ui.page_tor import page_tor from .ui.page_offers import page_offers, page_offer, page_newoffer +from .ui.page_wallet import page_wallets, page_wallet from .ui.page_automation import ( page_automation_strategies, page_automation_strategy, @@ -257,304 +257,6 @@ class HttpHandler(BaseHTTPRequestHandler): active_swaps=[(s[0].hex(), s[1], strBidState(s[2]), strTxState(s[3]), strTxState(s[4])) for s in active_swaps], ), 'UTF-8') - def page_wallets(self, url_split, post_string): - swap_client = self.server.swap_client - - page_data = {} - messages = [] - form_data = self.checkForm(post_string, 'wallets', messages) - if form_data: - for c in Coins: - if c not in chainparams: - continue - cid = str(int(c)) - - if bytes('newaddr_' + cid, 'utf-8') in form_data: - swap_client.cacheNewAddressForCoin(c) - elif bytes('reseed_' + cid, 'utf-8') in form_data: - try: - swap_client.reseedWallet(c) - messages.append('Reseed complete ' + str(c)) - except Exception as ex: - messages.append('Reseed failed ' + str(ex)) - swap_client.updateWalletsInfo(True, c) - elif bytes('withdraw_' + cid, 'utf-8') in form_data: - try: - value = form_data[bytes('amt_' + cid, 'utf-8')][0].decode('utf-8') - page_data['wd_value_' + cid] = value - except Exception as e: - messages.append('Error: Missing value') - try: - address = form_data[bytes('to_' + cid, 'utf-8')][0].decode('utf-8') - page_data['wd_address_' + cid] = address - except Exception as e: - messages.append('Error: Missing address') - - subfee = True if bytes('subfee_' + cid, 'utf-8') in form_data else False - page_data['wd_subfee_' + cid] = subfee - - if c == Coins.PART: - try: - type_from = form_data[bytes('withdraw_type_from_' + cid, 'utf-8')][0].decode('utf-8') - type_to = form_data[bytes('withdraw_type_to_' + cid, 'utf-8')][0].decode('utf-8') - page_data['wd_type_from_' + cid] = type_from - page_data['wd_type_to_' + cid] = type_to - except Exception as e: - messages.append('Error: Missing type') - - if len(messages) == 0: - ci = swap_client.ci(c) - ticker = ci.ticker() - if c == Coins.PART: - try: - txid = swap_client.withdrawParticl(type_from, type_to, value, address, subfee) - messages.append('Withdrew {} {} ({} to {}) to address {}
In txid: {}'.format(value, ticker, type_from, type_to, address, txid)) - except Exception as e: - messages.append('Error: {}'.format(str(e))) - else: - try: - txid = swap_client.withdrawCoin(c, value, address, subfee) - messages.append('Withdrew {} {} to address {}
In txid: {}'.format(value, ticker, address, txid)) - except Exception as e: - messages.append('Error: {}'.format(str(e))) - swap_client.updateWalletsInfo(True, c) - - swap_client.updateWalletsInfo() - wallets = swap_client.getCachedWalletsInfo() - - wallets_formatted = [] - sk = sorted(wallets.keys()) - - for k in sk: - w = wallets[k] - if 'error' in w: - wallets_formatted.append({ - 'cid': str(int(k)), - 'error': w['error'] - }) - continue - - if 'balance' not in w: - wallets_formatted.append({ - 'name': w['name'], - 'havedata': False, - 'updating': w['updating'], - }) - continue - - ci = swap_client.ci(k) - cid = str(int(k)) - wf = { - 'name': w['name'], - 'version': w['version'], - 'ticker': ci.ticker_mainnet(), - 'cid': cid, - 'balance': w['balance'], - 'blocks': w['blocks'], - 'synced': w['synced'], - 'deposit_address': w['deposit_address'], - 'expected_seed': w['expected_seed'], - 'balance_all': float(w['balance']) + float(w['unconfirmed']), - 'updating': w['updating'], - 'lastupdated': format_timestamp(w['lastupdated']), - 'havedata': True, - } - if float(w['unconfirmed']) > 0.0: - wf['unconfirmed'] = w['unconfirmed'] - - if k == Coins.PART: - wf['stealth_address'] = w['stealth_address'] - wf['blind_balance'] = w['blind_balance'] - if float(w['blind_unconfirmed']) > 0.0: - wf['blind_unconfirmed'] = w['blind_unconfirmed'] - wf['anon_balance'] = w['anon_balance'] - if float(w['anon_pending']) > 0.0: - wf['anon_pending'] = w['anon_pending'] - - wallets_formatted.append(wf) - - template = env.get_template('wallets.html') - return bytes(template.render( - title=self.server.title, - h2=self.server.title, - messages=messages, - wallets=wallets_formatted, - form_id=os.urandom(8).hex(), - ), 'UTF-8') - - def page_wallet(self, url_split, post_string): - ensure(len(url_split) > 2, 'Wallet not specified') - wallet_ticker = url_split[2] - swap_client = self.server.swap_client - - coin_id = getCoinIdFromTicker(wallet_ticker) - - page_data = {} - messages = [] - form_data = self.checkForm(post_string, 'settings', messages) - show_utxo_groups = False - if form_data: - cid = str(int(coin_id)) - - if bytes('newaddr_' + cid, 'utf-8') in form_data: - swap_client.cacheNewAddressForCoin(coin_id) - elif bytes('reseed_' + cid, 'utf-8') in form_data: - try: - swap_client.reseedWallet(coin_id) - messages.append('Reseed complete ' + str(coin_id)) - except Exception as ex: - messages.append('Reseed failed ' + str(ex)) - swap_client.updateWalletsInfo(True, coin_id) - elif bytes('withdraw_' + cid, 'utf-8') in form_data: - try: - value = form_data[bytes('amt_' + cid, 'utf-8')][0].decode('utf-8') - page_data['wd_value_' + cid] = value - except Exception as e: - messages.append('Error: Missing value') - try: - address = form_data[bytes('to_' + cid, 'utf-8')][0].decode('utf-8') - page_data['wd_address_' + cid] = address - except Exception as e: - messages.append('Error: Missing address') - - subfee = True if bytes('subfee_' + cid, 'utf-8') in form_data else False - page_data['wd_subfee_' + cid] = subfee - - if coin_id == Coins.PART: - try: - type_from = form_data[bytes('withdraw_type_from_' + cid, 'utf-8')][0].decode('utf-8') - type_to = form_data[bytes('withdraw_type_to_' + cid, 'utf-8')][0].decode('utf-8') - page_data['wd_type_from_' + cid] = type_from - page_data['wd_type_to_' + cid] = type_to - except Exception as e: - messages.append('Error: Missing type') - - if len(messages) == 0: - ci = swap_client.ci(coin_id) - ticker = ci.ticker() - if coin_id == Coins.PART: - try: - txid = swap_client.withdrawParticl(type_from, type_to, value, address, subfee) - messages.append('Withdrew {} {} ({} to {}) to address {}
In txid: {}'.format(value, ticker, type_from, type_to, address, txid)) - except Exception as e: - messages.append('Error: {}'.format(str(e))) - else: - try: - txid = swap_client.withdrawCoin(coin_id, value, address, subfee) - messages.append('Withdrew {} {} to address {}
In txid: {}'.format(value, ticker, address, txid)) - except Exception as e: - messages.append('Error: {}'.format(str(e))) - swap_client.updateWalletsInfo(True, coin_id) - elif have_data_entry(form_data, 'showutxogroups'): - show_utxo_groups = True - elif have_data_entry(form_data, 'create_utxo'): - show_utxo_groups = True - try: - value = get_data_entry(form_data, 'utxo_value') - page_data['utxo_value'] = value - - ci = swap_client.ci(coin_id) - - value_sats = ci.make_int(value) - - txid, address = ci.createUTXO(value_sats) - messages.append('Created new utxo of value {} and address {}
In txid: {}'.format(value, address, txid)) - except Exception as e: - messages.append('Error: {}'.format(str(e))) - if swap_client.debug is True: - swap_client.log.error(traceback.format_exc()) - - swap_client.updateWalletsInfo() - wallets = swap_client.getCachedWalletsInfo({'coin_id': coin_id}) - for k in wallets.keys(): - w = wallets[k] - if 'error' in w: - wallet_data = { - 'cid': str(int(k)), - 'error': w['error'] - } - continue - - if 'balance' not in w: - wallet_data = { - 'name': w['name'], - 'havedata': False, - 'updating': w['updating'], - } - continue - - ci = swap_client.ci(k) - fee_rate, fee_src = swap_client.getFeeRateForCoin(k) - est_fee = swap_client.estimateWithdrawFee(k, fee_rate) - cid = str(int(k)) - wallet_data = { - 'name': w['name'], - 'version': w['version'], - 'ticker': ci.ticker_mainnet(), - 'cid': cid, - 'fee_rate': ci.format_amount(int(fee_rate * ci.COIN())), - 'fee_rate_src': fee_src, - 'est_fee': 'Unknown' if est_fee is None else ci.format_amount(int(est_fee * ci.COIN())), - 'balance': w['balance'], - 'blocks': w['blocks'], - 'synced': w['synced'], - 'deposit_address': w['deposit_address'], - 'expected_seed': w['expected_seed'], - 'balance_all': float(w['balance']) + float(w['unconfirmed']), - 'updating': w['updating'], - 'lastupdated': format_timestamp(w['lastupdated']), - 'havedata': True, - } - if float(w['unconfirmed']) > 0.0: - wallet_data['unconfirmed'] = w['unconfirmed'] - - if k == Coins.PART: - wallet_data['stealth_address'] = w['stealth_address'] - wallet_data['blind_balance'] = w['blind_balance'] - if float(w['blind_unconfirmed']) > 0.0: - wallet_data['blind_unconfirmed'] = w['blind_unconfirmed'] - wallet_data['anon_balance'] = w['anon_balance'] - if float(w['anon_pending']) > 0.0: - wallet_data['anon_pending'] = w['anon_pending'] - - elif k == Coins.XMR: - wallet_data['main_address'] = w.get('main_address', 'Refresh necessary') - - if 'wd_type_from_' + cid in page_data: - wallet_data['wd_type_from'] = page_data['wd_type_from_' + cid] - if 'wd_type_to_' + cid in page_data: - wallet_data['wd_type_to'] = page_data['wd_type_to_' + cid] - - if 'wd_value_' + cid in page_data: - wallet_data['wd_value'] = page_data['wd_value_' + cid] - if 'wd_address_' + cid in page_data: - wallet_data['wd_address'] = page_data['wd_address_' + cid] - if 'wd_subfee_' + cid in page_data: - wallet_data['wd_subfee'] = page_data['wd_subfee_' + cid] - if 'utxo_value' in page_data: - wallet_data['utxo_value'] = page_data['utxo_value'] - - if show_utxo_groups: - utxo_groups = '' - - unspent_by_addr = swap_client.getUnspentsByAddr(k) - - sorted_unspent_by_addr = sorted(unspent_by_addr.items(), key=lambda x: x[1], reverse=True) - for kv in sorted_unspent_by_addr: - utxo_groups += kv[0] + ' ' + ci.format_amount(kv[1]) + '\n' - - wallet_data['show_utxo_groups'] = True - wallet_data['utxo_groups'] = utxo_groups - - template = env.get_template('wallet.html') - return bytes(template.render( - title=self.server.title, - h2=self.server.title, - messages=messages, - w=wallet_data, - form_id=os.urandom(8).hex(), - ), 'UTF-8') - def page_settings(self, url_split, post_string): swap_client = self.server.swap_client @@ -1000,9 +702,9 @@ class HttpHandler(BaseHTTPRequestHandler): if url_split[1] == 'active': return self.page_active(url_split, post_string) if url_split[1] == 'wallets': - return self.page_wallets(url_split, post_string) + return page_wallets(self, url_split, post_string) if url_split[1] == 'wallet': - return self.page_wallet(url_split, post_string) + return page_wallet(self, url_split, post_string) if url_split[1] == 'settings': return self.page_settings(url_split, post_string) if url_split[1] == 'rpc': diff --git a/basicswap/interface_xmr.py b/basicswap/interface_xmr.py index 5364f64..6952e02 100644 --- a/basicswap/interface_xmr.py +++ b/basicswap/interface_xmr.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -# Copyright (c) 2020-2021 tecnovert +# Copyright (c) 2020-2022 tecnovert # Distributed under the MIT software license, see the accompanying # file LICENSE or http://www.opensource.org/licenses/mit-license.php. @@ -117,22 +117,34 @@ class XMRInterface(CoinInterface): return self.rpc_wallet_cb('get_version')['version'] def getBlockchainInfo(self): - rv = {} - - # get_block_count returns "Internal error" if bootstrap-daemon is active - # rv['blocks'] = self.rpc_cb('get_block_count')['count'] - rv['blocks'] = self.rpc_cb2('get_height', timeout=30)['height'] - - # sync_info = self.rpc_cb('sync_info', timeout=30) - # rv['verificationprogress'] = 0.0 if 'spans' in sync_info else 1.0 - rv['verificationprogress'] = 0.0 + get_height = self.rpc_cb2('get_height', timeout=30) + 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['untrusted'] = True + get_info = self.rpc_cb2('get_info', timeout=30) + if 'height_without_bootstrap' in get_info: + rv['blocks'] = get_info['height_without_bootstrap'] + + rv['block_count'] = get_info['height'] + if rv['block_count'] > rv['blocks']: + rv['verificationprogress'] = rv['blocks'] / rv['block_count'] + else: + rv['block_count'] = self.rpc_cb('get_block_count', timeout=30)['count'] + rv['verificationprogress'] = rv['blocks'] / rv['block_count'] + except Exception as e: + self._log.warning('XMR get_block_count failed with: %s', str(e)) + rv['verificationprogress'] = 0.0 return rv def getChainHeight(self): - # get_block_count returns "Internal error" if bootstrap-daemon is active - # return self.rpc_cb('get_info')['height'] - # return self.rpc_cb('get_block_count')['count'] return self.rpc_cb2('get_height', timeout=30)['height'] def getWalletInfo(self): diff --git a/basicswap/js_server.py b/basicswap/js_server.py index fb80149..39d22d0 100644 --- a/basicswap/js_server.py +++ b/basicswap/js_server.py @@ -62,6 +62,7 @@ def withdraw_coin(swap_client, coin_type, post_string, is_json): def js_wallets(self, url_split, post_string, is_json): + swap_client = self.server.swap_client if len(url_split) > 3: ticker_str = url_split[3] coin_type = tickerToCoinId(ticker_str) @@ -69,9 +70,11 @@ def js_wallets(self, url_split, post_string, is_json): if len(url_split) > 4: cmd = url_split[4] if cmd == 'withdraw': - return bytes(json.dumps(withdraw_coin(self.server.swap_client, coin_type, post_string, is_json)), 'UTF-8') + return bytes(json.dumps(withdraw_coin(swap_client, coin_type, post_string, is_json)), 'UTF-8') raise ValueError('Unknown command') - return bytes(json.dumps(self.server.swap_client.getWalletInfo(coin_type)), 'UTF-8') + + rv = swap_client.getWalletInfo(coin_type).update(swap_client.getBlockchainInfo(coin_type)) + return bytes(json.dumps(rv), 'UTF-8') return bytes(json.dumps(self.server.swap_client.getWalletsInfo()), 'UTF-8') diff --git a/basicswap/ui/page_wallet.py b/basicswap/ui/page_wallet.py new file mode 100644 index 0000000..5cd5f04 --- /dev/null +++ b/basicswap/ui/page_wallet.py @@ -0,0 +1,327 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2022 tecnovert +# Distributed under the MIT software license, see the accompanying +# file LICENSE or http://www.opensource.org/licenses/mit-license.php. + +import os +import traceback + +from .util import ( + get_data_entry, + have_data_entry, +) +from basicswap.util import ( + ensure, + format_timestamp, +) +from basicswap.chainparams import ( + Coins, + chainparams, + getCoinIdFromTicker, +) + + +def page_wallets(self, url_split, post_string): + server = self.server + swap_client = server.swap_client + + page_data = {} + messages = [] + form_data = self.checkForm(post_string, 'wallets', messages) + if form_data: + for c in Coins: + if c not in chainparams: + continue + cid = str(int(c)) + + if bytes('newaddr_' + cid, 'utf-8') in form_data: + swap_client.cacheNewAddressForCoin(c) + elif bytes('reseed_' + cid, 'utf-8') in form_data: + try: + swap_client.reseedWallet(c) + messages.append('Reseed complete ' + str(c)) + except Exception as ex: + messages.append('Reseed failed ' + str(ex)) + swap_client.updateWalletsInfo(True, c) + elif bytes('withdraw_' + cid, 'utf-8') in form_data: + try: + value = form_data[bytes('amt_' + cid, 'utf-8')][0].decode('utf-8') + page_data['wd_value_' + cid] = value + except Exception as e: + messages.append('Error: Missing value') + try: + address = form_data[bytes('to_' + cid, 'utf-8')][0].decode('utf-8') + page_data['wd_address_' + cid] = address + except Exception as e: + messages.append('Error: Missing address') + + subfee = True if bytes('subfee_' + cid, 'utf-8') in form_data else False + page_data['wd_subfee_' + cid] = subfee + + if c == Coins.PART: + try: + type_from = form_data[bytes('withdraw_type_from_' + cid, 'utf-8')][0].decode('utf-8') + type_to = form_data[bytes('withdraw_type_to_' + cid, 'utf-8')][0].decode('utf-8') + page_data['wd_type_from_' + cid] = type_from + page_data['wd_type_to_' + cid] = type_to + except Exception as e: + messages.append('Error: Missing type') + + if len(messages) == 0: + ci = swap_client.ci(c) + ticker = ci.ticker() + if c == Coins.PART: + try: + txid = swap_client.withdrawParticl(type_from, type_to, value, address, subfee) + messages.append('Withdrew {} {} ({} to {}) to address {}
In txid: {}'.format(value, ticker, type_from, type_to, address, txid)) + except Exception as e: + messages.append('Error: {}'.format(str(e))) + else: + try: + txid = swap_client.withdrawCoin(c, value, address, subfee) + messages.append('Withdrew {} {} to address {}
In txid: {}'.format(value, ticker, address, txid)) + except Exception as e: + messages.append('Error: {}'.format(str(e))) + swap_client.updateWalletsInfo(True, c) + + swap_client.updateWalletsInfo() + wallets = swap_client.getCachedWalletsInfo() + + wallets_formatted = [] + sk = sorted(wallets.keys()) + + for k in sk: + w = wallets[k] + if 'error' in w: + wallets_formatted.append({ + 'cid': str(int(k)), + 'error': w['error'] + }) + continue + + if 'no_data' in w: + wallets_formatted.append({ + 'name': w['name'], + 'havedata': False, + 'updating': w['updating'], + }) + continue + + ci = swap_client.ci(k) + cid = str(int(k)) + wf = { + 'name': ci.coin_name(), + 'version': w.get('version', '?'), + 'ticker': ci.ticker_mainnet(), + 'cid': cid, + 'balance': w.get('balance', '?'), + 'blocks': w.get('blocks', '?'), + 'synced': w.get('synced', '?'), + 'deposit_address': w.get('deposit_address', '?'), + 'expected_seed': w.get('expected_seed', '?'), + 'updating': w.get('updating', '?'), + 'havedata': True, + } + + if 'balance' in w and 'unconfirmed' in w: + wf['balance_all'] = float(w['balance']) + float(w['unconfirmed']) + if 'lastupdated' in w: + wf['lastupdated'] = format_timestamp(w['lastupdated']) + if 'unconfirmed' in w and float(w['unconfirmed']) > 0.0: + wf['unconfirmed'] = w['unconfirmed'] + + if k == Coins.PART: + wf['stealth_address'] = w.get('stealth_address', '?') + wf['blind_balance'] = w.get('blind_balance', '?') + if 'blind_unconfirmed' in w and float(w['blind_unconfirmed']) > 0.0: + wf['blind_unconfirmed'] = w['blind_unconfirmed'] + wf['anon_balance'] = w.get('anon_balance', '?') + if 'anon_pending' in w and float(w['anon_pending']) > 0.0: + wf['anon_pending'] = w['anon_pending'] + + wallets_formatted.append(wf) + + template = server.env.get_template('wallets.html') + return bytes(template.render( + title=server.title, + h2=server.title, + messages=messages, + wallets=wallets_formatted, + form_id=os.urandom(8).hex(), + ), 'UTF-8') + + +def page_wallet(self, url_split, post_string): + ensure(len(url_split) > 2, 'Wallet not specified') + wallet_ticker = url_split[2] + server = self.server + swap_client = server.swap_client + + coin_id = getCoinIdFromTicker(wallet_ticker) + + page_data = {} + messages = [] + form_data = self.checkForm(post_string, 'settings', messages) + show_utxo_groups = False + if form_data: + cid = str(int(coin_id)) + + if bytes('newaddr_' + cid, 'utf-8') in form_data: + swap_client.cacheNewAddressForCoin(coin_id) + elif bytes('reseed_' + cid, 'utf-8') in form_data: + try: + swap_client.reseedWallet(coin_id) + messages.append('Reseed complete ' + str(coin_id)) + except Exception as ex: + messages.append('Reseed failed ' + str(ex)) + swap_client.updateWalletsInfo(True, coin_id) + elif bytes('withdraw_' + cid, 'utf-8') in form_data: + try: + value = form_data[bytes('amt_' + cid, 'utf-8')][0].decode('utf-8') + page_data['wd_value_' + cid] = value + except Exception as e: + messages.append('Error: Missing value') + try: + address = form_data[bytes('to_' + cid, 'utf-8')][0].decode('utf-8') + page_data['wd_address_' + cid] = address + except Exception as e: + messages.append('Error: Missing address') + + subfee = True if bytes('subfee_' + cid, 'utf-8') in form_data else False + page_data['wd_subfee_' + cid] = subfee + + if coin_id == Coins.PART: + try: + type_from = form_data[bytes('withdraw_type_from_' + cid, 'utf-8')][0].decode('utf-8') + type_to = form_data[bytes('withdraw_type_to_' + cid, 'utf-8')][0].decode('utf-8') + page_data['wd_type_from_' + cid] = type_from + page_data['wd_type_to_' + cid] = type_to + except Exception as e: + messages.append('Error: Missing type') + + if len(messages) == 0: + ci = swap_client.ci(coin_id) + ticker = ci.ticker() + if coin_id == Coins.PART: + try: + txid = swap_client.withdrawParticl(type_from, type_to, value, address, subfee) + messages.append('Withdrew {} {} ({} to {}) to address {}
In txid: {}'.format(value, ticker, type_from, type_to, address, txid)) + except Exception as e: + messages.append('Error: {}'.format(str(e))) + else: + try: + txid = swap_client.withdrawCoin(coin_id, value, address, subfee) + messages.append('Withdrew {} {} to address {}
In txid: {}'.format(value, ticker, address, txid)) + except Exception as e: + messages.append('Error: {}'.format(str(e))) + swap_client.updateWalletsInfo(True, coin_id) + elif have_data_entry(form_data, 'showutxogroups'): + show_utxo_groups = True + elif have_data_entry(form_data, 'create_utxo'): + show_utxo_groups = True + try: + value = get_data_entry(form_data, 'utxo_value') + page_data['utxo_value'] = value + + ci = swap_client.ci(coin_id) + + value_sats = ci.make_int(value) + + txid, address = ci.createUTXO(value_sats) + messages.append('Created new utxo of value {} and address {}
In txid: {}'.format(value, address, txid)) + except Exception as e: + messages.append('Error: {}'.format(str(e))) + if swap_client.debug is True: + swap_client.log.error(traceback.format_exc()) + + swap_client.updateWalletsInfo() + wallets = swap_client.getCachedWalletsInfo({'coin_id': coin_id}) + for k in wallets.keys(): + w = wallets[k] + if 'error' in w: + wallet_data = { + 'cid': str(int(k)), + 'error': w['error'] + } + continue + + if 'balance' not in w: + wallet_data = { + 'name': w['name'], + 'havedata': False, + 'updating': w['updating'], + } + continue + + ci = swap_client.ci(k) + fee_rate, fee_src = swap_client.getFeeRateForCoin(k) + est_fee = swap_client.estimateWithdrawFee(k, fee_rate) + cid = str(int(k)) + wallet_data = { + 'name': w['name'], + 'version': w['version'], + 'ticker': ci.ticker_mainnet(), + 'cid': cid, + 'fee_rate': ci.format_amount(int(fee_rate * ci.COIN())), + 'fee_rate_src': fee_src, + 'est_fee': 'Unknown' if est_fee is None else ci.format_amount(int(est_fee * ci.COIN())), + 'balance': w['balance'], + 'blocks': w['blocks'], + 'synced': w['synced'], + 'deposit_address': w['deposit_address'], + 'expected_seed': w['expected_seed'], + 'balance_all': float(w['balance']) + float(w['unconfirmed']), + 'updating': w['updating'], + 'lastupdated': format_timestamp(w['lastupdated']), + 'havedata': True, + } + if float(w['unconfirmed']) > 0.0: + wallet_data['unconfirmed'] = w['unconfirmed'] + + if k == Coins.PART: + wallet_data['stealth_address'] = w['stealth_address'] + wallet_data['blind_balance'] = w['blind_balance'] + if float(w['blind_unconfirmed']) > 0.0: + wallet_data['blind_unconfirmed'] = w['blind_unconfirmed'] + wallet_data['anon_balance'] = w['anon_balance'] + if float(w['anon_pending']) > 0.0: + wallet_data['anon_pending'] = w['anon_pending'] + + elif k == Coins.XMR: + wallet_data['main_address'] = w.get('main_address', 'Refresh necessary') + + if 'wd_type_from_' + cid in page_data: + wallet_data['wd_type_from'] = page_data['wd_type_from_' + cid] + if 'wd_type_to_' + cid in page_data: + wallet_data['wd_type_to'] = page_data['wd_type_to_' + cid] + + if 'wd_value_' + cid in page_data: + wallet_data['wd_value'] = page_data['wd_value_' + cid] + if 'wd_address_' + cid in page_data: + wallet_data['wd_address'] = page_data['wd_address_' + cid] + if 'wd_subfee_' + cid in page_data: + wallet_data['wd_subfee'] = page_data['wd_subfee_' + cid] + if 'utxo_value' in page_data: + wallet_data['utxo_value'] = page_data['utxo_value'] + + if show_utxo_groups: + utxo_groups = '' + + unspent_by_addr = swap_client.getUnspentsByAddr(k) + + sorted_unspent_by_addr = sorted(unspent_by_addr.items(), key=lambda x: x[1], reverse=True) + for kv in sorted_unspent_by_addr: + utxo_groups += kv[0] + ' ' + ci.format_amount(kv[1]) + '\n' + + wallet_data['show_utxo_groups'] = True + wallet_data['utxo_groups'] = utxo_groups + + template = server.env.get_template('wallet.html') + return bytes(template.render( + title=server.title, + h2=server.title, + messages=messages, + w=wallet_data, + form_id=os.urandom(8).hex(), + ), 'UTF-8')