diff --git a/basicswap/basicswap.py b/basicswap/basicswap.py index b17d7ea..55de02d 100644 --- a/basicswap/basicswap.py +++ b/basicswap/basicswap.py @@ -52,6 +52,7 @@ from .util import ( format_amount, format_timestamp, DeserialiseNum, + zeroIfNone, make_int, ensure, ) @@ -143,6 +144,8 @@ from .basicswap_util import ( getLastBidState, isActiveBidState, NotificationTypes as NT, + AutomationOverrideOptions, + VisibilityOverrideOptions, ) @@ -157,12 +160,6 @@ def validOfferStateToReceiveBid(offer_state): return False -def zeroIfNone(value): - if value is None: - return 0 - return value - - def threadPollXMRChainState(swap_client, coin_type): ci = swap_client.ci(coin_type) cc = swap_client.coin_clients[coin_type] @@ -1189,17 +1186,57 @@ class BasicSwap(BaseApp): try: now = int(time.time()) session = self.openSession() - q = session.execute(f'SELECT COUNT(*) FROM knownidentities WHERE address = "{address}"').first() + q = session.execute('SELECT COUNT(*) FROM knownidentities WHERE address = :address', {'address': address}).first() if q[0] < 1: - q = session.execute(f'INSERT INTO knownidentities (active_ind, address, created_at) VALUES (1, "{address}", {now})') + session.execute('INSERT INTO knownidentities (active_ind, address, created_at) VALUES (1, :address, :now)', {'address': address, 'now': now}) - values = [] - pattern = '' if 'label' in data: - pattern += (', ' if pattern != '' else '') - pattern += 'label = "{}"'.format(data['label']) - values.append(address) - q = session.execute(f'UPDATE knownidentities SET {pattern} WHERE address = "{address}"') + session.execute('UPDATE knownidentities SET label = :label WHERE address = :address', {'address': address, 'label': data['label']}) + + if 'automation_override' in data: + new_value: int = 0 + data_value = data['automation_override'] + if isinstance(data_value, int): + new_value = data_value + elif isinstance(data_value, str): + if data_value.isdigit(): + new_value = int(data_value) + elif data_value == 'default': + new_value = 0 + elif data_value == 'always_accept': + new_value = int(AutomationOverrideOptions.ALWAYS_ACCEPT) + elif data_value == 'never_accept': + new_value = int(AutomationOverrideOptions.NEVER_ACCEPT) + else: + raise ValueError('Unknown automation_override value') + else: + raise ValueError('Unknown automation_override type') + + session.execute('UPDATE knownidentities SET automation_override = :new_value WHERE address = :address', {'address': address, 'new_value': new_value}) + + if 'visibility_override' in data: + new_value: int = 0 + data_value = data['visibility_override'] + if isinstance(data_value, int): + new_value = data_value + elif isinstance(data_value, str): + if data_value.isdigit(): + new_value = int(data_value) + elif data_value == 'default': + new_value = 0 + elif data_value == 'hide': + new_value = int(VisibilityOverrideOptions.HIDE) + elif data_value == 'block': + new_value = int(VisibilityOverrideOptions.BLOCK) + else: + raise ValueError('Unknown visibility_override value') + else: + raise ValueError('Unknown visibility_override type') + + session.execute('UPDATE knownidentities SET visibility_override = :new_value WHERE address = :address', {'address': address, 'new_value': new_value}) + + if 'note' in data: + session.execute('UPDATE knownidentities SET note = :note WHERE address = :address', {'address': address, 'note': data['note']}) finally: self.closeSession(session) @@ -1209,7 +1246,8 @@ class BasicSwap(BaseApp): session = self.openSession() query_str = 'SELECT address, label, num_sent_bids_successful, num_recv_bids_successful, ' + \ - ' num_sent_bids_rejected, num_recv_bids_rejected, num_sent_bids_failed, num_recv_bids_failed ' + \ + ' num_sent_bids_rejected, num_recv_bids_rejected, num_sent_bids_failed, num_recv_bids_failed, ' + \ + ' automation_override, visibility_override, note ' + \ ' FROM knownidentities ' + \ ' WHERE active_ind = 1 ' @@ -1240,6 +1278,9 @@ class BasicSwap(BaseApp): 'num_recv_bids_rejected': zeroIfNone(row[5]), 'num_sent_bids_failed': zeroIfNone(row[6]), 'num_recv_bids_failed': zeroIfNone(row[7]), + 'automation_override': zeroIfNone(row[8]), + 'visibility_override': zeroIfNone(row[9]), + 'note': row[10], } rv.append(identity) return rv @@ -2186,22 +2227,6 @@ class BasicSwap(BaseApp): session.remove() self.mxDB.release() - def updateIdentity(self, address, label): - self.mxDB.acquire() - try: - session = scoped_session(self.session_factory) - identity = session.query(KnownIdentity).filter_by(address=address).first() - if identity is None: - identity = KnownIdentity(active_ind=1, address=address) - identity.label = label - identity.updated_at = int(time.time()) - session.add(identity) - session.commit() - finally: - session.close() - session.remove() - self.mxDB.release() - def list_bid_events(self, bid_id, session): query_str = 'SELECT created_at, event_type, event_msg FROM eventlog ' + \ 'WHERE active_ind = 1 AND linked_type = {} AND linked_id = x\'{}\' '.format(Concepts.BID, bid_id.hex()) @@ -4158,6 +4183,24 @@ class BasicSwap(BaseApp): total_value += amount return bids, total_value + def evaluateKnownIdentityForAutoAccept(self, strategy, identity_stats) -> bool: + if identity_stats: + if identity_stats.automation_override == AutomationOverrideOptions.NEVER_ACCEPT: + raise AutomationConstraint('From address is marked never accept') + if identity_stats.automation_override == AutomationOverrideOptions.ALWAYS_ACCEPT: + return True + + if strategy.only_known_identities: + if not identity_stats: + raise AutomationConstraint('Unknown bidder') + + # TODO: More options + if identity_stats.num_recv_bids_successful < 1: + raise AutomationConstraint('Bidder has too few successful swaps') + if identity_stats.num_recv_bids_successful <= identity_stats.num_recv_bids_failed: + raise AutomationConstraint('Bidder has too many failed swaps') + return True + def shouldAutoAcceptBid(self, offer, bid, session=None): try: use_session = self.openSession(session) @@ -4195,16 +4238,8 @@ class BasicSwap(BaseApp): if num_not_completed >= max_concurrent_bids: raise AutomationConstraint('Already have {} bids to complete'.format(num_not_completed)) - if strategy.only_known_identities: - identity_stats = use_session.query(KnownIdentity).filter_by(address=bid.bid_addr).first() - if not identity_stats: - raise AutomationConstraint('Unknown bidder') - - # TODO: More options - if identity_stats.num_recv_bids_successful < 1: - raise AutomationConstraint('Bidder has too few successful swaps') - if identity_stats.num_recv_bids_successful <= identity_stats.num_recv_bids_failed: - raise AutomationConstraint('Bidder has too many failed swaps') + identity_stats = use_session.query(KnownIdentity).filter_by(address=bid.bid_addr).first() + self.evaluateKnownIdentityForAutoAccept(strategy, identity_stats) self.logEvent(Concepts.BID, bid.bid_id, diff --git a/basicswap/basicswap_util.py b/basicswap/basicswap_util.py index f8cb7da..34a700c 100644 --- a/basicswap/basicswap_util.py +++ b/basicswap/basicswap_util.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2021-2022 tecnovert +# Copyright (c) 2021-2023 tecnovert # Distributed under the MIT software license, see the accompanying # file LICENSE or http://www.opensource.org/licenses/mit-license.php. @@ -192,6 +192,45 @@ class DebugTypes(IntEnum): B_LOCK_TX_MISSED_SEND = auto() +class NotificationTypes(IntEnum): + NONE = 0 + OFFER_RECEIVED = auto() + BID_RECEIVED = auto() + BID_ACCEPTED = auto() + + +class AutomationOverrideOptions(IntEnum): + DEFAULT = 0 + ALWAYS_ACCEPT = 1 + NEVER_ACCEPT = auto() + + +def strAutomationOverrideOption(option): + if option == AutomationOverrideOptions.DEFAULT: + return 'Default' + if option == AutomationOverrideOptions.ALWAYS_ACCEPT: + return 'Always Accept' + if option == AutomationOverrideOptions.NEVER_ACCEPT: + return 'Never Accept' + return 'Unknown' + + +class VisibilityOverrideOptions(IntEnum): + DEFAULT = 0 + HIDE = 1 + BLOCK = auto() + + +def strVisibilityOverrideOption(option): + if option == VisibilityOverrideOptions.DEFAULT: + return 'Default' + if option == VisibilityOverrideOptions.HIDE: + return 'Hide' + if option == VisibilityOverrideOptions.BLOCK: + return 'Block' + return 'Unknown' + + def strOfferState(state): if state == OfferStates.OFFER_SENT: return 'Sent' @@ -202,13 +241,6 @@ def strOfferState(state): return 'Unknown' -class NotificationTypes(IntEnum): - NONE = 0 - OFFER_RECEIVED = auto() - BID_RECEIVED = auto() - BID_ACCEPTED = auto() - - def strBidState(state): if state == BidStates.BID_SENT: return 'Sent' diff --git a/basicswap/db.py b/basicswap/db.py index 4bb8198..b11d581 100644 --- a/basicswap/db.py +++ b/basicswap/db.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2019-2022 tecnovert +# Copyright (c) 2019-2023 tecnovert # Distributed under the MIT software license, see the accompanying # file LICENSE or http://www.opensource.org/licenses/mit-license.php. @@ -12,7 +12,7 @@ from enum import IntEnum, auto from sqlalchemy.ext.declarative import declarative_base -CURRENT_DB_VERSION = 17 +CURRENT_DB_VERSION = 18 CURRENT_DB_DATA_VERSION = 2 Base = declarative_base() @@ -421,6 +421,8 @@ class KnownIdentity(Base): num_recv_bids_rejected = sa.Column(sa.Integer) num_sent_bids_failed = sa.Column(sa.Integer) num_recv_bids_failed = sa.Column(sa.Integer) + automation_override = sa.Column(sa.Integer) # AutomationOverrideOptions + visibility_override = sa.Column(sa.Integer) # VisibilityOverrideOptions note = sa.Column(sa.String) 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 d44bf69..538feb7 100644 --- a/basicswap/db_upgrades.py +++ b/basicswap/db_upgrades.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2022 tecnovert +# Copyright (c) 2022-2023 tecnovert # Distributed under the MIT software license, see the accompanying # file LICENSE or http://www.opensource.org/licenses/mit-license.php. @@ -238,6 +238,11 @@ def upgradeDatabase(self, db_version): tx_data BLOB, used_by BLOB, PRIMARY KEY (record_id))''') + elif current_version == 16: + db_version += 1 + session.execute('ALTER TABLE knownidentities ADD COLUMN automation_override INTEGER') + session.execute('ALTER TABLE knownidentities ADD COLUMN visibility_override INTEGER') + session.execute('UPDATE knownidentities SET active_ind = 1') if current_version != db_version: self.db_version = db_version diff --git a/basicswap/http_server.py b/basicswap/http_server.py index a06f3b4..aeb466d 100644 --- a/basicswap/http_server.py +++ b/basicswap/http_server.py @@ -17,6 +17,7 @@ from . import __version__ from .util import ( dumpj, ensure, + zeroIfNone, LockedCoinError, format_timestamp, ) @@ -25,9 +26,11 @@ from .chainparams import ( chainparams, ) from .basicswap_util import ( - strBidState, strTxState, + strBidState, strAddressType, + AutomationOverrideOptions, + strAutomationOverrideOption, ) from .js_server import ( @@ -37,6 +40,7 @@ from .js_server import ( from .ui.util import ( getCoinName, get_data_entry, + get_data_entry_or, have_data_entry, listAvailableCoins, ) @@ -447,10 +451,13 @@ class HttpHandler(BaseHTTPRequestHandler): if have_data_entry(form_data, 'edit'): page_data['show_edit_form'] = True if have_data_entry(form_data, 'apply'): - new_label = get_data_entry(form_data, 'label') - try: - swap_client.updateIdentity(identity_address, new_label) + data = { + 'label': get_data_entry_or(form_data, 'label', ''), + 'note': get_data_entry_or(form_data, 'note', ''), + 'automation_override': get_data_entry(form_data, 'automation_override'), + } + swap_client.setIdentityData({'address': identity_address}, data) messages.append('Updated') except Exception as e: err_messages.append(str(e)) @@ -466,14 +473,19 @@ class HttpHandler(BaseHTTPRequestHandler): page_data['num_recv_bids_rejected'] = identity.num_recv_bids_rejected page_data['num_sent_bids_failed'] = identity.num_sent_bids_failed page_data['num_recv_bids_failed'] = identity.num_recv_bids_failed + automation_override = zeroIfNone(identity.automation_override) + page_data['automation_override'] = automation_override + page_data['str_automation_override'] = strAutomationOverrideOption(automation_override) + page_data['note'] = identity.note except Exception as e: - messages.append(e) + err_messages.append(e) template = env.get_template('identity.html') return self.render_template(template, { 'messages': messages, 'err_messages': err_messages, 'data': page_data, + 'automation_override_options': [(int(opt), strAutomationOverrideOption(opt)) for opt in AutomationOverrideOptions if opt > 0], 'summary': summary, }) diff --git a/basicswap/js_server.py b/basicswap/js_server.py index 95cebda..99dcf57 100644 --- a/basicswap/js_server.py +++ b/basicswap/js_server.py @@ -517,6 +517,12 @@ def js_identities(self, url_split, post_string: str, is_json: bool) -> bytes: set_data = {} if have_data_entry(post_data, 'set_label'): set_data['label'] = get_data_entry(post_data, 'set_label') + if have_data_entry(post_data, 'set_automation_override'): + set_data['automation_override'] = get_data_entry(post_data, 'set_automation_override') + if have_data_entry(post_data, 'set_visibility_override'): + set_data['visibility_override'] = get_data_entry(post_data, 'set_visibility_override') + if have_data_entry(post_data, 'set_note'): + set_data['note'] = get_data_entry(post_data, 'set_note') if set_data: ensure('address' in filters, 'Must provide an address to modify data') @@ -631,7 +637,7 @@ pages = { 'rateslist': js_rates_list, 'generatenotification': js_generatenotification, 'notifications': js_notifications, - 'identities': js_identities, + 'identities': js_identities, 'vacuumdb': js_vacuumdb, 'getcoinseed': js_getcoinseed, 'setpassword': js_setpassword, diff --git a/basicswap/templates/identity.html b/basicswap/templates/identity.html index 8f46aa6..6b79eee 100644 --- a/basicswap/templates/identity.html +++ b/basicswap/templates/identity.html @@ -63,11 +63,38 @@ +