diff --git a/basicswap/basicswap.py b/basicswap/basicswap.py index a9334fc..98e5d94 100644 --- a/basicswap/basicswap.py +++ b/basicswap/basicswap.py @@ -558,6 +558,12 @@ class BasicSwap(BaseApp): elif current_version == 9: session.execute('ALTER TABLE wallets ADD COLUMN wallet_data VARCHAR') db_version += 1 + elif current_version == 10: + session.execute('ALTER TABLE smsgaddresses ADD COLUMN active_ind INTEGER') + session.execute('ALTER TABLE smsgaddresses ADD COLUMN created_at INTEGER') + session.execute('ALTER TABLE smsgaddresses ADD COLUMN note VARCHAR') + session.execute('UPDATE smsgaddresses SET active_ind = 1, created_at = 1') + db_version += 1 if current_version != db_version: self.db_version = db_version @@ -981,7 +987,7 @@ class BasicSwap(BaseApp): session.add(offer) session.add(SentOffer(offer_id=offer_id)) if addr_send_from is None: - session.add(SmsgAddress(addr=offer_addr, use_type=MessageTypes.OFFER)) + session.add(SmsgAddress(addr=offer_addr, use_type=MessageTypes.OFFER, active_ind=1, created_at=offer_created_at)) session.commit() finally: @@ -1536,7 +1542,7 @@ class BasicSwap(BaseApp): session = scoped_session(self.session_factory) self.saveBidInSession(bid_id, bid, session) if addr_send_from is None: - session.add(SmsgAddress(addr=bid_addr, use_type=MessageTypes.BID)) + session.add(SmsgAddress(addr=bid_addr, use_type=MessageTypes.BID, active_ind=1, created_at=now)) session.commit() finally: session.close() @@ -1907,7 +1913,7 @@ class BasicSwap(BaseApp): session = scoped_session(self.session_factory) self.saveBidInSession(xmr_swap.bid_id, bid, session, xmr_swap) if addr_send_from is None: - session.add(SmsgAddress(addr=bid_addr, use_type=MessageTypes.BID)) + session.add(SmsgAddress(addr=bid_addr, use_type=MessageTypes.BID, active_ind=1, created_at=bid_created_at)) session.commit() finally: session.close() @@ -5040,13 +5046,73 @@ class BasicSwap(BaseApp): finally: self.mxDB.release() + def listAllSMSGAddresses(self, addr_id=None): + filters = '' + if addr_id is not None: + filters += f' WHERE addr_id = {addr_id} ' + self.mxDB.acquire() + try: + session = scoped_session(self.session_factory) + rv = [] + query_str = f'SELECT addr_id, addr, use_type, active_ind, created_at, note FROM smsgaddresses {filters} ORDER BY created_at' + + q = session.execute(query_str) + for row in q: + rv.append({ + 'id': row[0], + 'addr': row[1], + 'type': row[2], + 'active_ind': row[3], + 'created_at': row[4], + 'note': row[5], + }) + return rv + finally: + session.close() + session.remove() + self.mxDB.release() + + #listening_keys = self.callcoinrpc(Coins.PART, 'smsglocalkeys', []) + return [] + + def addSMSGAddress(self, addressnote=None): + # TODO: smsg addresses should be generated from a unique chain + self.mxDB.acquire() + try: + session = scoped_session(self.session_factory) + now = int(time.time()) + new_addr = self.callrpc('getnewaddress') + self.callrpc('smsgaddlocaladdress', [new_addr]) # Enable receiving smsgs + + session.add(SmsgAddress(addr=new_addr, use_type=MessageTypes.OFFER, active_ind=1, created_at=now, note=addressnote)) + session.commit() + return new_addr + finally: + session.close() + session.remove() + self.mxDB.release() + + def editSMSGAddress(self, address, active_ind, addressnote): + self.mxDB.acquire() + try: + session = scoped_session(self.session_factory) + mode = '-' if active_ind == 0 else '+' + self.callrpc('smsglocalkeys', ['recv', mode, address]) + + session.execute('UPDATE smsgaddresses SET active_ind = {}, note = "{}" WHERE addr = "{}"'.format(active_ind, addressnote, address)) + session.commit() + finally: + session.close() + session.remove() + self.mxDB.release() + def listSmsgAddresses(self, use_type_str): use_type = MessageTypes.OFFER if use_type_str == 'offer' else MessageTypes.BID self.mxDB.acquire() try: session = scoped_session(self.session_factory) rv = [] - q = session.execute('SELECT addr FROM smsgaddresses WHERE use_type = {} ORDER BY addr_id DESC'.format(use_type)) + q = session.execute('SELECT addr FROM smsgaddresses WHERE use_type = {} AND active_ind = 1 ORDER BY addr_id DESC'.format(use_type)) for row in q: rv.append(row[0]) return rv diff --git a/basicswap/basicswap_util.py b/basicswap/basicswap_util.py index bb82fa8..f366329 100644 --- a/basicswap/basicswap_util.py +++ b/basicswap/basicswap_util.py @@ -239,6 +239,14 @@ def strTxType(tx_type): return 'Unknown' +def strMessageType(msg_type): + if msg_type == MessageTypes.OFFER: + return 'Offers' + if msg_type == MessageTypes.BID: + return 'Bids' + return 'Unknown' + + def getLockName(lock_type): if lock_type == SEQUENCE_LOCK_BLOCKS: return 'Sequence lock, blocks' diff --git a/basicswap/db.py b/basicswap/db.py index aa3edd6..d7d7909 100644 --- a/basicswap/db.py +++ b/basicswap/db.py @@ -12,7 +12,7 @@ from enum import IntEnum, auto from sqlalchemy.ext.declarative import declarative_base -CURRENT_DB_VERSION = 10 +CURRENT_DB_VERSION = 11 Base = declarative_base() @@ -215,8 +215,11 @@ class SmsgAddress(Base): __tablename__ = 'smsgaddresses' addr_id = sa.Column(sa.Integer, primary_key=True, autoincrement=True) - addr = sa.Column(sa.String) + active_ind = sa.Column(sa.Integer) + created_at = sa.Column(sa.BigInteger) + addr = sa.Column(sa.String, unique=True) use_type = sa.Column(sa.Integer) + note = sa.Column(sa.String) class EventQueue(Base): diff --git a/basicswap/http_server.py b/basicswap/http_server.py index cd37f9b..dbac543 100644 --- a/basicswap/http_server.py +++ b/basicswap/http_server.py @@ -27,6 +27,7 @@ from .basicswap_util import ( strOfferState, strBidState, strTxState, + strMessageType, getLockName, SEQUENCE_LOCK_TIME, ABS_LOCK_TIME, @@ -40,6 +41,7 @@ from .js_server import ( js_sentbids, js_network, js_revokeoffer, + js_smsgaddresses, js_index, ) from .ui import ( @@ -77,6 +79,16 @@ def listAvailableCoins(swap_client): return coins +def validateTextInput(text, name, messages, max_length=None): + if max_length is not None and len(text) > max_length: + messages.append(f'Error: {name} is too long') + return False + if len(text) > 0 and text.isalnum() is False: + messages.append(f'Error: {name} must consist of only letters and digits') + return False + return True + + def extractDomain(url): return url.split('://', 1)[1].split('/', 1)[0] @@ -930,6 +942,67 @@ class HttpHandler(BaseHTTPRequestHandler): watched_outputs=[(wo[1].hex(), getCoinName(wo[0]), wo[2], wo[3], int(wo[4])) for wo in watched_outputs], ), 'UTF-8') + def page_smsgaddresses(self, url_split, post_string): + swap_client = self.server.swap_client + + page_data = {} + messages = [] + smsgaddresses = [] + + listaddresses = True + form_data = self.checkForm(post_string, 'smsgaddresses', messages) + if form_data: + edit_address_id = None + for key in form_data: + if key.startswith(b'editaddr_'): + edit_address_id = int(key.split(b'_')[1]) + break + if edit_address_id is not None: + listaddresses = False + page_data['edit_address'] = edit_address_id + page_data['addr_data'] = swap_client.listAllSMSGAddresses(addr_id=edit_address_id)[0] + elif b'saveaddr' in form_data: + edit_address_id = int(form_data[b'edit_address_id'][0].decode('utf-8')) + edit_addr = form_data[b'edit_address'][0].decode('utf-8') + active_ind = int(form_data[b'active_ind'][0].decode('utf-8')) + assert(active_ind == 0 or active_ind == 1), 'Invalid sort by' + addressnote = '' if b'addressnote' not in form_data else form_data[b'addressnote'][0].decode('utf-8') + if not validateTextInput(addressnote, 'Address note', messages, max_length=30): + listaddresses = False + page_data['edit_address'] = edit_address_id + else: + swap_client.editSMSGAddress(edit_addr, active_ind=active_ind, addressnote=addressnote) + messages.append(f'Edited address {edit_addr}') + elif b'shownewaddr' in form_data: + listaddresses = False + page_data['new_address'] = True + elif b'createnewaddr' in form_data: + addressnote = '' if b'addressnote' not in form_data else form_data[b'addressnote'][0].decode('utf-8') + if not validateTextInput(addressnote, 'Address note', messages, max_length=30): + listaddresses = False + page_data['new_address'] = True + else: + new_addr = swap_client.addSMSGAddress(addressnote=addressnote) + messages.append(f'Created address {new_addr}') + + if listaddresses is True: + smsgaddresses = swap_client.listAllSMSGAddresses() + network_addr = swap_client.network_addr + + for addr in smsgaddresses: + addr['type'] = strMessageType(addr['type']) + + template = env.get_template('smsgaddresses.html') + return bytes(template.render( + title=self.server.title, + h2=self.server.title, + messages=messages, + data=page_data, + form_id=os.urandom(8).hex(), + smsgaddresses=smsgaddresses, + network_addr=network_addr, + ), 'UTF-8') + def page_shutdown(self, url_split, post_string): swap_client = self.server.swap_client swap_client.stopRunning() @@ -976,6 +1049,7 @@ class HttpHandler(BaseHTTPRequestHandler): 'sentbids': js_sentbids, 'network': js_network, 'revokeoffer': js_revokeoffer, + 'smsgaddresses': js_smsgaddresses, }.get(url_split[2], js_index) return func(self, url_split, post_string, is_json) except Exception as ex: @@ -1035,6 +1109,8 @@ class HttpHandler(BaseHTTPRequestHandler): return self.page_bids(url_split, post_string, sent=True) if url_split[1] == 'watched': return self.page_watched(url_split, post_string) + if url_split[1] == 'smsgaddresses': + return self.page_smsgaddresses(url_split, post_string) if url_split[1] == 'shutdown': return self.page_shutdown(url_split, post_string) return self.page_index(url_split) diff --git a/basicswap/js_server.py b/basicswap/js_server.py index 21f4e03..f879788 100644 --- a/basicswap/js_server.py +++ b/basicswap/js_server.py @@ -245,3 +245,27 @@ def js_revokeoffer(self, url_split, post_string, is_json): def js_index(self, url_split, post_string, is_json): return bytes(json.dumps(self.server.swap_client.getSummary()), 'UTF-8') + + +def js_smsgaddresses(self, url_split, post_string, is_json): + swap_client = self.server.swap_client + if len(url_split) > 3: + if post_string == '': + raise ValueError('No post data') + if is_json: + post_data = json.loads(post_string) + post_data['is_json'] = True + else: + post_data = urllib.parse.parse_qs(post_string) + if url_split[3] == 'new': + addressnote = get_data_entry_or(post_data, 'addressnote', '') + new_addr = swap_client.addSMSGAddress(addressnote) + return bytes(json.dumps({'new_address': new_addr}), 'UTF-8') + elif url_split[3] == 'edit': + address = get_data_entry(post_data, 'address') + activeind = int(get_data_entry(post_data, 'active_ind')) + addressnote = get_data_entry_or(post_data, 'addressnote', '') + new_addr = swap_client.editSMSGAddress(address, activeind, addressnote) + return bytes(json.dumps({'edited_address': address}), 'UTF-8') + + return bytes(json.dumps(swap_client.listAllSMSGAddresses()), 'UTF-8') diff --git a/basicswap/templates/index.html b/basicswap/templates/index.html index 22a11cb..1604053 100644 --- a/basicswap/templates/index.html +++ b/basicswap/templates/index.html @@ -11,6 +11,7 @@ Version: {{ version }} Settings
RPC Console
Explorers
+SMSG Addresses

Swaps in progress: {{ summary.num_swapping }}
Network Offers: {{ summary.num_network_offers }}
diff --git a/basicswap/templates/smsgaddresses.html b/basicswap/templates/smsgaddresses.html new file mode 100644 index 0000000..7e8efde --- /dev/null +++ b/basicswap/templates/smsgaddresses.html @@ -0,0 +1,47 @@ +{% include 'header.html' %} + +

Active SMSG Addresses

+ +{% for m in messages %} +

{{ m }}

+{% endfor %} + +
+ +{% if data.edit_address %} + + + +

Edit Address {{ data.addr_data.addr }}

+ + + + + +
Active
Note
+{% elif data.new_address %} +

New Address

+ + + + +
Note
+{% else %} + + + +{% for sa in smsgaddresses %} + +{% endfor %} +
AddressTypeActiveCreated AtNoteAction
{{ network_addr }}Network Address
{{ sa.addr }}{{ sa.type }}{{ sa.active_ind }}{{ sa.created_at | formatts }}{{ sa.note }}
+ + +{% endif %} + +
+ +

home

+ diff --git a/tests/basicswap/test_xmr.py b/tests/basicswap/test_xmr.py index d1feef3..6e63903 100644 --- a/tests/basicswap/test_xmr.py +++ b/tests/basicswap/test_xmr.py @@ -466,6 +466,74 @@ class Test(unittest.TestCase): end_xmr = float(js_0_end['6']['balance']) + float(js_0_end['6']['unconfirmed']) assert(end_xmr > 10.9 and end_xmr < 11.0) + + def test_011_smsgaddresses(self): + js_1 = json.loads(urlopen('http://127.0.0.1:1801/json/smsgaddresses').read()) + + post_json = { + 'addressnote': 'testing', + } + json_rv = json.loads(post_json_req('http://127.0.0.1:1801/json/smsgaddresses/new', post_json)) + new_address = json_rv['new_address'] + + js_2 = json.loads(urlopen('http://127.0.0.1:1801/json/smsgaddresses').read()) + assert(len(js_2) == len(js_1) + 1) + found = False + for addr in js_2: + if addr['addr'] == new_address: + assert(addr['note'] == 'testing') + found = True + assert(found is True) + + found = False + lks = callnoderpc(1, 'smsglocalkeys') + for key in lks['wallet_keys']: + if key['address'] == new_address: + assert(key['receive'] == '1') + found = True + assert(found is True) + + # Disable + post_json = { + 'address': new_address, + 'addressnote': 'testing2', + 'active_ind': '0', + } + json_rv = json.loads(post_json_req('http://127.0.0.1:1801/json/smsgaddresses/edit', post_json)) + assert(json_rv['edited_address'] == new_address) + + js_3 = json.loads(urlopen('http://127.0.0.1:1801/json/smsgaddresses').read()) + found = False + for addr in js_3: + if addr['addr'] == new_address: + assert(addr['note'] == 'testing2') + assert(addr['active_ind'] == 0) + found = True + assert(found is True) + + found = False + lks = callnoderpc(1, 'smsglocalkeys') + for key in lks['wallet_keys']: + if key['address'] == new_address: + found = True + assert(found is False) + + # Reenable + post_json = { + 'address': new_address, + 'active_ind': '1', + } + json_rv = json.loads(post_json_req('http://127.0.0.1:1801/json/smsgaddresses/edit', post_json)) + assert(json_rv['edited_address'] == new_address) + + found = False + lks = callnoderpc(1, 'smsglocalkeys') + for key in lks['wallet_keys']: + if key['address'] == new_address: + assert(key['receive'] == '1') + found = True + assert(found is True) + def test_02_leader_recover_a_lock_tx(self): logging.info('---------- Test PART to XMR leader recovers coin a lock tx') swap_clients = self.swap_clients