mirror of
https://github.com/basicswap/basicswap.git
synced 2025-11-07 11:18:10 +01:00
network: Use Simplex direct chats.
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -36,6 +36,11 @@ class KeyTypes(IntEnum):
|
||||
KAF = 6
|
||||
|
||||
|
||||
class MessageNetworks(IntEnum):
|
||||
SMSG = auto()
|
||||
SIMPLEX = auto()
|
||||
|
||||
|
||||
class MessageTypes(IntEnum):
|
||||
OFFER = auto()
|
||||
BID = auto()
|
||||
@@ -53,6 +58,8 @@ class MessageTypes(IntEnum):
|
||||
ADS_BID_LF = auto()
|
||||
ADS_BID_ACCEPT_FL = auto()
|
||||
|
||||
CONNECT_REQ = auto()
|
||||
|
||||
|
||||
class AddressTypes(IntEnum):
|
||||
OFFER = auto()
|
||||
@@ -111,6 +118,7 @@ class BidStates(IntEnum):
|
||||
BID_EXPIRED = 31
|
||||
BID_AACCEPT_DELAY = 32
|
||||
BID_AACCEPT_FAIL = 33
|
||||
CONNECT_REQ_SENT = 34
|
||||
|
||||
|
||||
class TxStates(IntEnum):
|
||||
@@ -228,6 +236,10 @@ class NotificationTypes(IntEnum):
|
||||
BID_ACCEPTED = auto()
|
||||
|
||||
|
||||
class ConnectionRequestTypes(IntEnum):
|
||||
BID = 1
|
||||
|
||||
|
||||
class AutomationOverrideOptions(IntEnum):
|
||||
DEFAULT = 0
|
||||
ALWAYS_ACCEPT = 1
|
||||
@@ -339,6 +351,8 @@ def strBidState(state):
|
||||
return "Auto accept delay"
|
||||
if state == BidStates.BID_AACCEPT_FAIL:
|
||||
return "Auto accept failed"
|
||||
if state == BidStates.CONNECT_REQ_SENT:
|
||||
return "Connect request sent"
|
||||
return "Unknown" + " " + str(state)
|
||||
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ from enum import IntEnum, auto
|
||||
from typing import Optional
|
||||
|
||||
|
||||
CURRENT_DB_VERSION = 28
|
||||
CURRENT_DB_VERSION = 29
|
||||
CURRENT_DB_DATA_VERSION = 6
|
||||
|
||||
|
||||
@@ -219,6 +219,7 @@ class Bid(Table):
|
||||
bid_addr = Column("string")
|
||||
pk_bid_addr = Column("blob")
|
||||
proof_address = Column("string")
|
||||
proof_signature = Column("blob")
|
||||
proof_utxos = Column("blob")
|
||||
# Address to spend lock tx to - address from wallet if empty TODO
|
||||
withdraw_to_addr = Column("string")
|
||||
@@ -658,6 +659,41 @@ class CoinRates(Table):
|
||||
last_updated = Column("integer")
|
||||
|
||||
|
||||
class MessageNetworks(Table):
|
||||
__tablename__ = "message_networks"
|
||||
|
||||
record_id = Column("integer", primary_key=True, autoincrement=True)
|
||||
active_ind = Column("integer")
|
||||
name = Column("string")
|
||||
created_at = Column("integer")
|
||||
|
||||
|
||||
class DirectMessageRoute(Table):
|
||||
__tablename__ = "direct_message_routes"
|
||||
|
||||
record_id = Column("integer", primary_key=True, autoincrement=True)
|
||||
active_ind = Column("integer")
|
||||
network_id = Column("integer")
|
||||
linked_type = Column("integer")
|
||||
linked_id = Column("blob")
|
||||
smsg_addr_local = Column("string")
|
||||
smsg_addr_remote = Column("string")
|
||||
# smsg_addr_id_local = Column("integer") # SmsgAddress
|
||||
# smsg_addr_id_remote = Column("integer") # KnownIdentity
|
||||
route_data = Column("blob")
|
||||
created_at = Column("integer")
|
||||
|
||||
|
||||
class DirectMessageRouteLink(Table):
|
||||
__tablename__ = "direct_message_route_links"
|
||||
record_id = Column("integer", primary_key=True, autoincrement=True)
|
||||
active_ind = Column("integer")
|
||||
direct_message_route_id = Column("integer")
|
||||
linked_type = Column("integer")
|
||||
linked_id = Column("blob")
|
||||
created_at = Column("integer")
|
||||
|
||||
|
||||
def create_db_(con, log) -> None:
|
||||
c = con.cursor()
|
||||
|
||||
@@ -915,6 +951,7 @@ class DBMethods:
|
||||
query += f"{key}=:{key}"
|
||||
|
||||
cursor.execute(query, values)
|
||||
return cursor.lastrowid
|
||||
|
||||
def query(
|
||||
self,
|
||||
|
||||
@@ -76,6 +76,10 @@ def remove_expired_data(self, time_offset: int = 0):
|
||||
"DELETE FROM message_links WHERE linked_type = :type_ind AND linked_id = :linked_id",
|
||||
{"type_ind": int(Concepts.BID), "linked_id": bid_row[0]},
|
||||
)
|
||||
cursor.execute(
|
||||
"DELETE FROM direct_message_route_links WHERE linked_type = :type_ind AND linked_id = :linked_id",
|
||||
{"type_ind": int(Concepts.BID), "linked_id": bid_row[0]},
|
||||
)
|
||||
|
||||
cursor.execute(
|
||||
"DELETE FROM eventlog WHERE eventlog.linked_type = :type_ind AND eventlog.linked_id = :offer_id",
|
||||
|
||||
@@ -858,7 +858,7 @@ def js_automationstrategies(self, url_split, post_string: str, is_json: bool) ->
|
||||
"label": strat_data.label,
|
||||
"type_ind": strat_data.type_ind,
|
||||
"only_known_identities": strat_data.only_known_identities,
|
||||
"data": json.loads(strat_data.data.decode("utf-8")),
|
||||
"data": json.loads(strat_data.data.decode("UTF-8")),
|
||||
"note": "" if strat_data.note is None else strat_data.note,
|
||||
}
|
||||
return bytes(json.dumps(rv), "UTF-8")
|
||||
@@ -992,7 +992,7 @@ def js_unlock(self, url_split, post_string, is_json) -> bytes:
|
||||
swap_client = self.server.swap_client
|
||||
post_data = getFormData(post_string, is_json)
|
||||
|
||||
password = get_data_entry(post_data, "password")
|
||||
password: str = get_data_entry(post_data, "password")
|
||||
|
||||
if have_data_entry(post_data, "coin"):
|
||||
coin = getCoinType(str(get_data_entry(post_data, "coin")))
|
||||
@@ -1167,6 +1167,49 @@ def js_coinprices(self, url_split, post_string, is_json) -> bytes:
|
||||
)
|
||||
|
||||
|
||||
def js_messageroutes(self, url_split, post_string, is_json) -> bytes:
|
||||
swap_client = self.server.swap_client
|
||||
post_data = {} if post_string == "" else getFormData(post_string, is_json)
|
||||
|
||||
filters = {
|
||||
"page_no": 1,
|
||||
"limit": PAGE_LIMIT,
|
||||
"sort_by": "created_at",
|
||||
"sort_dir": "desc",
|
||||
}
|
||||
|
||||
if have_data_entry(post_data, "sort_by"):
|
||||
sort_by = get_data_entry(post_data, "sort_by")
|
||||
ensure(
|
||||
sort_by
|
||||
in [
|
||||
"created_at",
|
||||
],
|
||||
"Invalid sort by",
|
||||
)
|
||||
filters["sort_by"] = sort_by
|
||||
if have_data_entry(post_data, "sort_dir"):
|
||||
sort_dir = get_data_entry(post_data, "sort_dir")
|
||||
ensure(sort_dir in ["asc", "desc"], "Invalid sort dir")
|
||||
filters["sort_dir"] = sort_dir
|
||||
|
||||
if have_data_entry(post_data, "offset"):
|
||||
filters["offset"] = int(get_data_entry(post_data, "offset"))
|
||||
if have_data_entry(post_data, "limit"):
|
||||
filters["limit"] = int(get_data_entry(post_data, "limit"))
|
||||
ensure(filters["limit"] > 0, "Invalid limit")
|
||||
|
||||
if have_data_entry(post_data, "address_from"):
|
||||
filters["address_from"] = get_data_entry(post_data, "address_from")
|
||||
if have_data_entry(post_data, "address_to"):
|
||||
filters["address_to"] = get_data_entry(post_data, "address_to")
|
||||
|
||||
action = get_data_entry_or(post_data, "action", None)
|
||||
|
||||
message_routes = swap_client.listMessageRoutes(filters, action)
|
||||
return bytes(json.dumps(message_routes), "UTF-8")
|
||||
|
||||
|
||||
endpoints = {
|
||||
"coins": js_coins,
|
||||
"wallets": js_wallets,
|
||||
@@ -1194,6 +1237,7 @@ endpoints = {
|
||||
"readurl": js_readurl,
|
||||
"active": js_active,
|
||||
"coinprices": js_coinprices,
|
||||
"messageroutes": js_messageroutes,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2024 tecnovert
|
||||
# Copyright (c) 2025 The Basicswap developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
@@ -23,6 +24,13 @@ protobuf ParseFromString would reset the whole object, from_bytes won't.
|
||||
from basicswap.util.integer import encode_varint, decode_varint
|
||||
|
||||
|
||||
NPBW_INT = 0
|
||||
NPBW_BYTES = 2
|
||||
|
||||
NPBF_STR = 1
|
||||
NPBF_BOOL = 2
|
||||
|
||||
|
||||
class NonProtobufClass:
|
||||
def __init__(self, init_all: bool = True, **kwargs):
|
||||
for key, value in kwargs.items():
|
||||
@@ -34,7 +42,7 @@ class NonProtobufClass:
|
||||
found_field = True
|
||||
break
|
||||
if found_field is False:
|
||||
raise ValueError(f"got an unexpected keyword argument '{key}'")
|
||||
raise ValueError(f"Got an unexpected keyword argument '{key}'")
|
||||
|
||||
if init_all:
|
||||
self.init_fields()
|
||||
@@ -117,151 +125,160 @@ class NonProtobufClass:
|
||||
|
||||
class OfferMessage(NonProtobufClass):
|
||||
_map = {
|
||||
1: ("protocol_version", 0, 0),
|
||||
2: ("coin_from", 0, 0),
|
||||
3: ("coin_to", 0, 0),
|
||||
4: ("amount_from", 0, 0),
|
||||
5: ("amount_to", 0, 0),
|
||||
6: ("min_bid_amount", 0, 0),
|
||||
7: ("time_valid", 0, 0),
|
||||
8: ("lock_type", 0, 0),
|
||||
9: ("lock_value", 0, 0),
|
||||
10: ("swap_type", 0, 0),
|
||||
11: ("proof_address", 2, 1),
|
||||
12: ("proof_signature", 2, 1),
|
||||
13: ("pkhash_seller", 2, 0),
|
||||
14: ("secret_hash", 2, 0),
|
||||
15: ("fee_rate_from", 0, 0),
|
||||
16: ("fee_rate_to", 0, 0),
|
||||
17: ("amount_negotiable", 0, 2),
|
||||
18: ("rate_negotiable", 0, 2),
|
||||
19: ("proof_utxos", 2, 0),
|
||||
1: ("protocol_version", NPBW_INT, 0),
|
||||
2: ("coin_from", NPBW_INT, 0),
|
||||
3: ("coin_to", NPBW_INT, 0),
|
||||
4: ("amount_from", NPBW_INT, 0),
|
||||
5: ("amount_to", NPBW_INT, 0),
|
||||
6: ("min_bid_amount", NPBW_INT, 0),
|
||||
7: ("time_valid", NPBW_INT, 0),
|
||||
8: ("lock_type", NPBW_INT, 0),
|
||||
9: ("lock_value", NPBW_INT, 0),
|
||||
10: ("swap_type", NPBW_INT, 0),
|
||||
11: ("proof_address", NPBW_BYTES, NPBF_STR),
|
||||
12: ("proof_signature", NPBW_BYTES, NPBF_STR),
|
||||
13: ("pkhash_seller", NPBW_BYTES, 0),
|
||||
14: ("secret_hash", NPBW_BYTES, 0),
|
||||
15: ("fee_rate_from", NPBW_INT, 0),
|
||||
16: ("fee_rate_to", NPBW_INT, 0),
|
||||
17: ("amount_negotiable", NPBW_INT, NPBF_BOOL),
|
||||
18: ("rate_negotiable", NPBW_INT, NPBF_BOOL),
|
||||
19: ("proof_utxos", NPBW_BYTES, 0),
|
||||
20: ("auto_accept_type", 0, 0),
|
||||
}
|
||||
|
||||
|
||||
class BidMessage(NonProtobufClass):
|
||||
_map = {
|
||||
1: ("protocol_version", 0, 0),
|
||||
2: ("offer_msg_id", 2, 0),
|
||||
3: ("time_valid", 0, 0),
|
||||
4: ("amount", 0, 0),
|
||||
5: ("amount_to", 0, 0),
|
||||
6: ("pkhash_buyer", 2, 0),
|
||||
7: ("proof_address", 2, 1),
|
||||
8: ("proof_signature", 2, 1),
|
||||
9: ("proof_utxos", 2, 0),
|
||||
10: ("pkhash_buyer_to", 2, 0),
|
||||
1: ("protocol_version", NPBW_INT, 0),
|
||||
2: ("offer_msg_id", NPBW_BYTES, 0),
|
||||
3: ("time_valid", NPBW_INT, 0),
|
||||
4: ("amount", NPBW_INT, 0),
|
||||
5: ("amount_to", NPBW_INT, 0),
|
||||
6: ("pkhash_buyer", NPBW_BYTES, 0),
|
||||
7: ("proof_address", NPBW_BYTES, NPBF_STR),
|
||||
8: ("proof_signature", NPBW_BYTES, NPBF_STR),
|
||||
9: ("proof_utxos", NPBW_BYTES, 0),
|
||||
10: ("pkhash_buyer_to", NPBW_BYTES, 0),
|
||||
}
|
||||
|
||||
|
||||
class BidAcceptMessage(NonProtobufClass):
|
||||
# Step 3, seller -> buyer
|
||||
_map = {
|
||||
1: ("bid_msg_id", 2, 0),
|
||||
2: ("initiate_txid", 2, 0),
|
||||
3: ("contract_script", 2, 0),
|
||||
4: ("pkhash_seller", 2, 0),
|
||||
1: ("bid_msg_id", NPBW_BYTES, 0),
|
||||
2: ("initiate_txid", NPBW_BYTES, 0),
|
||||
3: ("contract_script", NPBW_BYTES, 0),
|
||||
4: ("pkhash_seller", NPBW_BYTES, 0),
|
||||
}
|
||||
|
||||
|
||||
class OfferRevokeMessage(NonProtobufClass):
|
||||
_map = {
|
||||
1: ("offer_msg_id", 2, 0),
|
||||
2: ("signature", 2, 0),
|
||||
1: ("offer_msg_id", NPBW_BYTES, 0),
|
||||
2: ("signature", NPBW_BYTES, 0),
|
||||
}
|
||||
|
||||
|
||||
class BidRejectMessage(NonProtobufClass):
|
||||
_map = {
|
||||
1: ("bid_msg_id", 2, 0),
|
||||
2: ("reject_code", 0, 0),
|
||||
1: ("bid_msg_id", NPBW_BYTES, 0),
|
||||
2: ("reject_code", NPBW_INT, 0),
|
||||
}
|
||||
|
||||
|
||||
class XmrBidMessage(NonProtobufClass):
|
||||
# MSG1L, F -> L
|
||||
_map = {
|
||||
1: ("protocol_version", 0, 0),
|
||||
2: ("offer_msg_id", 2, 0),
|
||||
3: ("time_valid", 0, 0),
|
||||
4: ("amount", 0, 0),
|
||||
5: ("amount_to", 0, 0),
|
||||
6: ("pkaf", 2, 0),
|
||||
7: ("kbvf", 2, 0),
|
||||
8: ("kbsf_dleag", 2, 0),
|
||||
9: ("dest_af", 2, 0),
|
||||
1: ("protocol_version", NPBW_INT, 0),
|
||||
2: ("offer_msg_id", NPBW_BYTES, 0),
|
||||
3: ("time_valid", NPBW_INT, 0),
|
||||
4: ("amount", NPBW_INT, 0),
|
||||
5: ("amount_to", NPBW_INT, 0),
|
||||
6: ("pkaf", NPBW_BYTES, 0),
|
||||
7: ("kbvf", NPBW_BYTES, 0),
|
||||
8: ("kbsf_dleag", NPBW_BYTES, 0),
|
||||
9: ("dest_af", NPBW_BYTES, 0),
|
||||
}
|
||||
|
||||
|
||||
class XmrSplitMessage(NonProtobufClass):
|
||||
_map = {
|
||||
1: ("msg_id", 2, 0),
|
||||
2: ("msg_type", 0, 0),
|
||||
3: ("sequence", 0, 0),
|
||||
4: ("dleag", 2, 0),
|
||||
1: ("msg_id", NPBW_BYTES, 0),
|
||||
2: ("msg_type", NPBW_INT, 0),
|
||||
3: ("sequence", NPBW_INT, 0),
|
||||
4: ("dleag", NPBW_BYTES, 0),
|
||||
}
|
||||
|
||||
|
||||
class XmrBidAcceptMessage(NonProtobufClass):
|
||||
_map = {
|
||||
1: ("bid_msg_id", 2, 0),
|
||||
2: ("pkal", 2, 0),
|
||||
3: ("kbvl", 2, 0),
|
||||
4: ("kbsl_dleag", 2, 0),
|
||||
1: ("bid_msg_id", NPBW_BYTES, 0),
|
||||
2: ("pkal", NPBW_BYTES, 0),
|
||||
3: ("kbvl", NPBW_BYTES, 0),
|
||||
4: ("kbsl_dleag", NPBW_BYTES, 0),
|
||||
# MSG2F
|
||||
5: ("a_lock_tx", 2, 0),
|
||||
6: ("a_lock_tx_script", 2, 0),
|
||||
7: ("a_lock_refund_tx", 2, 0),
|
||||
8: ("a_lock_refund_tx_script", 2, 0),
|
||||
9: ("a_lock_refund_spend_tx", 2, 0),
|
||||
10: ("al_lock_refund_tx_sig", 2, 0),
|
||||
5: ("a_lock_tx", NPBW_BYTES, 0),
|
||||
6: ("a_lock_tx_script", NPBW_BYTES, 0),
|
||||
7: ("a_lock_refund_tx", NPBW_BYTES, 0),
|
||||
8: ("a_lock_refund_tx_script", NPBW_BYTES, 0),
|
||||
9: ("a_lock_refund_spend_tx", NPBW_BYTES, 0),
|
||||
10: ("al_lock_refund_tx_sig", NPBW_BYTES, 0),
|
||||
}
|
||||
|
||||
|
||||
class XmrBidLockTxSigsMessage(NonProtobufClass):
|
||||
# MSG3L
|
||||
_map = {
|
||||
1: ("bid_msg_id", 2, 0),
|
||||
2: ("af_lock_refund_spend_tx_esig", 2, 0),
|
||||
3: ("af_lock_refund_tx_sig", 2, 0),
|
||||
1: ("bid_msg_id", NPBW_BYTES, 0),
|
||||
2: ("af_lock_refund_spend_tx_esig", NPBW_BYTES, 0),
|
||||
3: ("af_lock_refund_tx_sig", NPBW_BYTES, 0),
|
||||
}
|
||||
|
||||
|
||||
class XmrBidLockSpendTxMessage(NonProtobufClass):
|
||||
# MSG4F
|
||||
_map = {
|
||||
1: ("bid_msg_id", 2, 0),
|
||||
2: ("a_lock_spend_tx", 2, 0),
|
||||
3: ("kal_sig", 2, 0),
|
||||
1: ("bid_msg_id", NPBW_BYTES, 0),
|
||||
2: ("a_lock_spend_tx", NPBW_BYTES, 0),
|
||||
3: ("kal_sig", NPBW_BYTES, 0),
|
||||
}
|
||||
|
||||
|
||||
class XmrBidLockReleaseMessage(NonProtobufClass):
|
||||
# MSG5F
|
||||
_map = {
|
||||
1: ("bid_msg_id", 2, 0),
|
||||
2: ("al_lock_spend_tx_esig", 2, 0),
|
||||
1: ("bid_msg_id", NPBW_BYTES, 0),
|
||||
2: ("al_lock_spend_tx_esig", NPBW_BYTES, 0),
|
||||
}
|
||||
|
||||
|
||||
class ADSBidIntentMessage(NonProtobufClass):
|
||||
# L -> F Sent from bidder, construct a reverse bid
|
||||
_map = {
|
||||
1: ("protocol_version", 0, 0),
|
||||
2: ("offer_msg_id", 2, 0),
|
||||
3: ("time_valid", 0, 0),
|
||||
4: ("amount_from", 0, 0),
|
||||
5: ("amount_to", 0, 0),
|
||||
1: ("protocol_version", NPBW_INT, 0),
|
||||
2: ("offer_msg_id", NPBW_BYTES, 0),
|
||||
3: ("time_valid", NPBW_INT, 0),
|
||||
4: ("amount_from", NPBW_INT, 0),
|
||||
5: ("amount_to", NPBW_INT, 0),
|
||||
}
|
||||
|
||||
|
||||
class ADSBidIntentAcceptMessage(NonProtobufClass):
|
||||
# F -> L Sent from offerer, construct a reverse bid
|
||||
_map = {
|
||||
1: ("bid_msg_id", 2, 0),
|
||||
2: ("pkaf", 2, 0),
|
||||
3: ("kbvf", 2, 0),
|
||||
4: ("kbsf_dleag", 2, 0),
|
||||
5: ("dest_af", 2, 0),
|
||||
1: ("bid_msg_id", NPBW_BYTES, 0),
|
||||
2: ("pkaf", NPBW_BYTES, 0),
|
||||
3: ("kbvf", NPBW_BYTES, 0),
|
||||
4: ("kbsf_dleag", NPBW_BYTES, 0),
|
||||
5: ("dest_af", NPBW_BYTES, 0),
|
||||
}
|
||||
|
||||
|
||||
class ConnectReqMessage(NonProtobufClass):
|
||||
_map = {
|
||||
1: ("network_type", NPBW_INT, 0),
|
||||
2: ("network_data", NPBW_BYTES, 0),
|
||||
3: ("request_type", NPBW_INT, 0),
|
||||
4: ("request_data", NPBW_BYTES, 0),
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import base64
|
||||
import json
|
||||
import threading
|
||||
import traceback
|
||||
import websocket
|
||||
|
||||
|
||||
@@ -25,9 +26,6 @@ from basicswap.util.address import (
|
||||
b58decode,
|
||||
decodeWif,
|
||||
)
|
||||
from basicswap.basicswap_util import (
|
||||
BidStates,
|
||||
)
|
||||
|
||||
|
||||
def encode_base64(data: bytes) -> str:
|
||||
@@ -52,6 +50,20 @@ class WebSocketThread(threading.Thread):
|
||||
|
||||
self.recv_queue = Queue()
|
||||
self.cmd_recv_queue = Queue()
|
||||
self.delayed_events_queue = Queue()
|
||||
|
||||
self.ignore_events: bool = False
|
||||
|
||||
self.num_messages_received: int = 0
|
||||
|
||||
def disable_debug_mode(self):
|
||||
self.ignore_events = False
|
||||
for i in range(100):
|
||||
try:
|
||||
message = self.delayed_events_queue.get(block=False)
|
||||
except Empty:
|
||||
break
|
||||
self.recv_queue.put(message)
|
||||
|
||||
def on_message(self, ws, message):
|
||||
if self.logger:
|
||||
@@ -62,6 +74,7 @@ class WebSocketThread(threading.Thread):
|
||||
if message.startswith('{"corrId"'):
|
||||
self.cmd_recv_queue.put(message)
|
||||
else:
|
||||
self.num_messages_received += 1
|
||||
self.recv_queue.put(message)
|
||||
|
||||
def queue_get(self):
|
||||
@@ -106,6 +119,18 @@ class WebSocketThread(threading.Thread):
|
||||
self.ws.send(cmd)
|
||||
return self.corrId
|
||||
|
||||
def wait_for_command_response(self, cmd_id):
|
||||
cmd_id = str(cmd_id)
|
||||
for i in range(100):
|
||||
message = self.cmd_queue_get()
|
||||
if message is not None:
|
||||
data = json.loads(message)
|
||||
if "corrId" in data:
|
||||
if data["corrId"] == cmd_id:
|
||||
return data
|
||||
self.delay_event.wait(0.5)
|
||||
raise ValueError(f"waitForResponse timed-out waiting for id: {cmd_id}")
|
||||
|
||||
def run(self):
|
||||
self.ws = websocket.WebSocketApp(
|
||||
self.url,
|
||||
@@ -130,7 +155,6 @@ def waitForResponse(ws_thread, sent_id, delay_event):
|
||||
message = ws_thread.cmd_queue_get()
|
||||
if message is not None:
|
||||
data = json.loads(message)
|
||||
# print(f"json: {json.dumps(data, indent=4)}")
|
||||
if "corrId" in data:
|
||||
if data["corrId"] == sent_id:
|
||||
return data
|
||||
@@ -174,10 +198,17 @@ def getPrivkeyForAddress(self, addr) -> bytes:
|
||||
raise ValueError("key not found")
|
||||
|
||||
|
||||
def sendSimplexMsg(
|
||||
self, network, addr_from: str, addr_to: str, payload: bytes, msg_valid: int, cursor
|
||||
def encryptMsg(
|
||||
self,
|
||||
addr_from: str,
|
||||
addr_to: str,
|
||||
payload: bytes,
|
||||
msg_valid: int,
|
||||
cursor,
|
||||
timestamp=None,
|
||||
deterministic=False,
|
||||
) -> bytes:
|
||||
self.log.debug("sendSimplexMsg")
|
||||
self.log.debug("encryptMsg")
|
||||
|
||||
try:
|
||||
rv = self.callrpc(
|
||||
@@ -210,12 +241,38 @@ def sendSimplexMsg(
|
||||
privkey_from = getPrivkeyForAddress(self, addr_from)
|
||||
|
||||
payload += bytes((0,)) # Include null byte to match smsg
|
||||
smsg_msg: bytes = smsgEncrypt(privkey_from, pubkey_to, payload)
|
||||
smsg_msg: bytes = smsgEncrypt(
|
||||
privkey_from, pubkey_to, payload, timestamp, deterministic
|
||||
)
|
||||
|
||||
return smsg_msg
|
||||
|
||||
|
||||
def sendSimplexMsg(
|
||||
self,
|
||||
network,
|
||||
addr_from: str,
|
||||
addr_to: str,
|
||||
payload: bytes,
|
||||
msg_valid: int,
|
||||
cursor,
|
||||
timestamp: int = None,
|
||||
deterministic: bool = False,
|
||||
to_user_name: str = None,
|
||||
) -> bytes:
|
||||
self.log.debug("sendSimplexMsg")
|
||||
|
||||
smsg_msg: bytes = encryptMsg(
|
||||
self, addr_from, addr_to, payload, msg_valid, cursor, timestamp, deterministic
|
||||
)
|
||||
smsg_id = smsgGetID(smsg_msg)
|
||||
|
||||
ws_thread = network["ws_thread"]
|
||||
sent_id = ws_thread.send_command("#bsx " + encode_base64(smsg_msg))
|
||||
if to_user_name is not None:
|
||||
to = "@" + to_user_name + " "
|
||||
else:
|
||||
to = "#bsx "
|
||||
sent_id = ws_thread.send_command(to + encode_base64(smsg_msg))
|
||||
response = waitForResponse(ws_thread, sent_id, self.delay_event)
|
||||
if response["resp"]["type"] != "newChatItems":
|
||||
json_str = json.dumps(response, indent=4)
|
||||
@@ -243,8 +300,10 @@ def decryptSimplexMsg(self, msg_data):
|
||||
|
||||
# Try with all active bid/offer addresses
|
||||
query: str = """SELECT DISTINCT address FROM (
|
||||
SELECT bid_addr AS address FROM bids WHERE active_ind = 1
|
||||
AND (in_progress = 1 OR (state > :bid_received AND state < :bid_completed) OR (state IN (:bid_received, :bid_sent) AND expire_at > :now))
|
||||
SELECT b.bid_addr AS address FROM bids b
|
||||
JOIN bidstates s ON b.state = s.state_id
|
||||
WHERE b.active_ind = 1
|
||||
AND (s.in_progress OR (s.swap_ended = 0 AND b.expire_at > :now))
|
||||
UNION
|
||||
SELECT addr_from AS address FROM offers WHERE active_ind = 1 AND expire_at > :now
|
||||
)"""
|
||||
@@ -253,15 +312,7 @@ def decryptSimplexMsg(self, msg_data):
|
||||
|
||||
try:
|
||||
cursor = self.openDB()
|
||||
addr_rows = cursor.execute(
|
||||
query,
|
||||
{
|
||||
"bid_received": int(BidStates.BID_RECEIVED),
|
||||
"bid_completed": int(BidStates.SWAP_COMPLETED),
|
||||
"bid_sent": int(BidStates.BID_SENT),
|
||||
"now": now,
|
||||
},
|
||||
).fetchall()
|
||||
addr_rows = cursor.execute(query, {"now": now}).fetchall()
|
||||
finally:
|
||||
self.closeDB(cursor, commit=False)
|
||||
|
||||
@@ -283,42 +334,97 @@ def decryptSimplexMsg(self, msg_data):
|
||||
return decrypted
|
||||
|
||||
|
||||
def parseSimplexMsg(self, chat_item):
|
||||
item_status = chat_item["chatItem"]["meta"]["itemStatus"]
|
||||
dir_type = item_status["type"]
|
||||
if dir_type not in ("sndRcvd", "rcvNew"):
|
||||
return None
|
||||
|
||||
snd_progress = item_status.get("sndProgress", None)
|
||||
if snd_progress and snd_progress != "complete":
|
||||
item_id = chat_item["chatItem"]["meta"]["itemId"]
|
||||
self.log.debug(f"simplex chat item {item_id} {snd_progress}")
|
||||
return None
|
||||
|
||||
conn_id = None
|
||||
msg_dir: str = "recv" if dir_type == "rcvNew" else "sent"
|
||||
chat_type: str = chat_item["chatInfo"]["type"]
|
||||
if chat_type == "group":
|
||||
chat_name = chat_item["chatInfo"]["groupInfo"]["localDisplayName"]
|
||||
conn_id = chat_item["chatInfo"]["groupInfo"]["groupId"]
|
||||
self.num_group_simplex_messages_received += 1
|
||||
elif chat_type == "direct":
|
||||
chat_name = chat_item["chatInfo"]["contact"]["localDisplayName"]
|
||||
conn_id = chat_item["chatInfo"]["contact"]["activeConn"]["connId"]
|
||||
self.num_direct_simplex_messages_received += 1
|
||||
else:
|
||||
return None
|
||||
|
||||
msg_content = chat_item["chatItem"]["content"]["msgContent"]["text"]
|
||||
try:
|
||||
msg_data: bytes = decode_base64(msg_content)
|
||||
decrypted_msg = decryptSimplexMsg(self, msg_data)
|
||||
if decrypted_msg is None:
|
||||
return None
|
||||
decrypted_msg["chat_type"] = chat_type
|
||||
decrypted_msg["chat_name"] = chat_name
|
||||
decrypted_msg["conn_id"] = conn_id
|
||||
decrypted_msg["msg_dir"] = msg_dir
|
||||
return decrypted_msg
|
||||
except Exception as e: # noqa: F841
|
||||
# self.log.debug(f"decryptSimplexMsg error: {e}")
|
||||
self.log.debug(f"decryptSimplexMsg error: {e}")
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def processEvent(self, ws_thread, msg_type: str, data) -> bool:
|
||||
if ws_thread.ignore_events:
|
||||
if msg_type not in ("contactConnected", "contactDeletedByContact"):
|
||||
return False
|
||||
ws_thread.delayed_events_queue.put(json.dumps(data))
|
||||
return True
|
||||
|
||||
if msg_type == "contactConnected":
|
||||
self.processContactConnected(data)
|
||||
elif msg_type == "contactDeletedByContact":
|
||||
self.processContactDisconnected(data)
|
||||
else:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def readSimplexMsgs(self, network):
|
||||
ws_thread = network["ws_thread"]
|
||||
|
||||
for i in range(100):
|
||||
message = ws_thread.queue_get()
|
||||
if message is None:
|
||||
break
|
||||
if self.delay_event.is_set():
|
||||
break
|
||||
|
||||
data = json.loads(message)
|
||||
# self.log.debug(f"message 1: {json.dumps(data, indent=4)}")
|
||||
# self.log.debug(f"Message: {json.dumps(data, indent=4)}")
|
||||
try:
|
||||
if data["resp"]["type"] in ("chatItemsStatusesUpdated", "newChatItems"):
|
||||
msg_type: str = data["resp"]["type"]
|
||||
if msg_type in ("chatItemsStatusesUpdated", "newChatItems"):
|
||||
for chat_item in data["resp"]["chatItems"]:
|
||||
item_status = chat_item["chatItem"]["meta"]["itemStatus"]
|
||||
if item_status["type"] in ("sndRcvd", "rcvNew"):
|
||||
snd_progress = item_status.get("sndProgress", None)
|
||||
if snd_progress:
|
||||
if snd_progress != "complete":
|
||||
item_id = chat_item["chatItem"]["meta"]["itemId"]
|
||||
self.log.debug(
|
||||
f"simplex chat item {item_id} {snd_progress}"
|
||||
)
|
||||
continue
|
||||
try:
|
||||
msg_data: bytes = decode_base64(
|
||||
chat_item["chatItem"]["content"]["msgContent"]["text"]
|
||||
)
|
||||
decrypted_msg = decryptSimplexMsg(self, msg_data)
|
||||
if decrypted_msg is None:
|
||||
continue
|
||||
self.processMsg(decrypted_msg)
|
||||
except Exception as e: # noqa: F841
|
||||
# self.log.debug(f"decryptSimplexMsg error: {e}")
|
||||
pass
|
||||
decrypted_msg = parseSimplexMsg(self, chat_item)
|
||||
if decrypted_msg is None:
|
||||
continue
|
||||
self.processMsg(decrypted_msg)
|
||||
elif msg_type == "chatError":
|
||||
# self.log.debug(f"chatError Message: {json.dumps(data, indent=4)}")
|
||||
pass
|
||||
elif processEvent(self, ws_thread, msg_type, data):
|
||||
pass
|
||||
else:
|
||||
self.log.debug(f"Unknown msg_type: {msg_type}")
|
||||
# self.log.debug(f"Message: {json.dumps(data, indent=4)}")
|
||||
except Exception as e:
|
||||
self.log.debug(f"readSimplexMsgs error: {e}")
|
||||
if self.debug:
|
||||
self.log.error(traceback.format_exc())
|
||||
|
||||
self.delay_event.wait(0.05)
|
||||
|
||||
@@ -348,3 +454,37 @@ def initialiseSimplexNetwork(self, network_config) -> None:
|
||||
}
|
||||
|
||||
self.active_networks.append(network)
|
||||
|
||||
|
||||
def closeSimplexChat(self, net_i, connId) -> bool:
|
||||
cmd_id = net_i.send_command("/chats")
|
||||
response = net_i.wait_for_command_response(cmd_id)
|
||||
remote_name = None
|
||||
for chat in response["resp"]["chats"]:
|
||||
if (
|
||||
"chatInfo" not in chat
|
||||
or "type" not in chat["chatInfo"]
|
||||
or chat["chatInfo"]["type"] != "direct"
|
||||
):
|
||||
continue
|
||||
try:
|
||||
if chat["chatInfo"]["contact"]["activeConn"]["connId"] == connId:
|
||||
remote_name = chat["chatInfo"]["contact"]["localDisplayName"]
|
||||
break
|
||||
except Exception as e:
|
||||
self.log.debug(f"Error parsing chat: {e}")
|
||||
|
||||
if remote_name is None:
|
||||
self.log.warning(
|
||||
f"Unable to find remote name for simplex direct chat, ID: {connId}"
|
||||
)
|
||||
return False
|
||||
|
||||
self.log.debug(f"Deleting simplex chat @{remote_name}, connID {connId}")
|
||||
cmd_id = net_i.send_command(f"/delete @{remote_name}")
|
||||
cmd_response = net_i.wait_for_command_response(cmd_id)
|
||||
if cmd_response["resp"]["type"] != "contactDeleted":
|
||||
self.log.warning(f"Failed to delete simplex chat, ID: {connId}")
|
||||
self.log.debug("cmd_response: {}".format(json.dumps(cmd_response, indent=4)))
|
||||
return False
|
||||
return True
|
||||
|
||||
@@ -76,7 +76,6 @@ def startSimplexClient(
|
||||
os.makedirs(data_path)
|
||||
|
||||
db_path = os.path.join(data_path, "simplex_client_data")
|
||||
|
||||
args = [bin_path, "-d", db_path, "-s", server_address, "-p", str(websocket_port)]
|
||||
|
||||
if not os.path.exists(db_path):
|
||||
|
||||
93
basicswap/ui/app.py
Normal file
93
basicswap/ui/app.py
Normal file
@@ -0,0 +1,93 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2025 The Basicswap developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
import json
|
||||
|
||||
from basicswap.db import getOrderByStr
|
||||
|
||||
|
||||
class UIApp:
|
||||
def listMessageRoutes(self, filters={}, action=None):
|
||||
cursor = self.openDB()
|
||||
try:
|
||||
rv = []
|
||||
|
||||
query_data: dict = {}
|
||||
filter_query_str: str = ""
|
||||
address_from: str = filters.get("address_from", None)
|
||||
if address_from is not None:
|
||||
filter_query_str += " AND smsg_addr_local = :address_from "
|
||||
query_data["address_from"] = address_from
|
||||
|
||||
address_to: str = filters.get("address_to", None)
|
||||
if address_from is not None:
|
||||
filter_query_str += " AND smsg_addr_remote = :address_to "
|
||||
query_data["address_to"] = address_to
|
||||
|
||||
if action is None:
|
||||
pass
|
||||
elif action == "clear":
|
||||
self.log.info("Clearing message routes")
|
||||
query_str: str = (
|
||||
"SELECT record_id, network_id, route_data"
|
||||
+ " FROM direct_message_routes "
|
||||
+ " WHERE active_ind = 1 "
|
||||
)
|
||||
query_str += filter_query_str
|
||||
rows = cursor.execute(query_str, query_data).fetchall()
|
||||
for row in rows:
|
||||
record_id, network_id, route_data = row
|
||||
route_data = json.loads(route_data.decode("UTF-8"))
|
||||
self.closeMessageRoute(record_id, network_id, route_data, cursor)
|
||||
|
||||
else:
|
||||
raise ValueError("Unknown action")
|
||||
|
||||
query_str: str = (
|
||||
"SELECT record_id, network_id, linked_type, linked_id, "
|
||||
+ " smsg_addr_local, smsg_addr_remote, route_data, created_at"
|
||||
+ " FROM direct_message_routes "
|
||||
+ " WHERE active_ind = 1 "
|
||||
)
|
||||
|
||||
query_str += filter_query_str
|
||||
query_str += getOrderByStr(filters)
|
||||
|
||||
limit = filters.get("limit", None)
|
||||
if limit is not None:
|
||||
query_str += " LIMIT :limit"
|
||||
query_data["limit"] = limit
|
||||
offset = filters.get("offset", None)
|
||||
if offset is not None:
|
||||
query_str += " OFFSET :offset"
|
||||
query_data["offset"] = offset
|
||||
|
||||
q = cursor.execute(query_str, query_data)
|
||||
rv = []
|
||||
for row in q:
|
||||
(
|
||||
record_id,
|
||||
network_id,
|
||||
linked_type,
|
||||
linked_id,
|
||||
smsg_addr_local,
|
||||
smsg_addr_remote,
|
||||
route_data,
|
||||
created_at,
|
||||
) = row
|
||||
rv.append(
|
||||
{
|
||||
"record_id": record_id,
|
||||
"network_id": network_id,
|
||||
"smsg_addr_local": smsg_addr_local,
|
||||
"smsg_addr_remote": smsg_addr_remote,
|
||||
"route_data": json.loads(route_data.decode("UTF-8")),
|
||||
}
|
||||
)
|
||||
|
||||
return rv
|
||||
finally:
|
||||
self.closeDB(cursor, commit=False)
|
||||
@@ -83,19 +83,35 @@ def smsgGetID(smsg_message: bytes) -> bytes:
|
||||
return smsg_timestamp.to_bytes(8, byteorder="big") + ripemd160(smsg_message[8:])
|
||||
|
||||
|
||||
def smsgEncrypt(privkey_from: bytes, pubkey_to: bytes, payload: bytes) -> bytes:
|
||||
def smsgEncrypt(
|
||||
privkey_from: bytes,
|
||||
pubkey_to: bytes,
|
||||
payload: bytes,
|
||||
smsg_timestamp: int = None,
|
||||
deterministic: bool = False,
|
||||
) -> bytes:
|
||||
# assert len(payload) < 128 # Requires lz4 if payload > 128 bytes
|
||||
# TODO: Add lz4 to match core smsg
|
||||
smsg_timestamp = int(time.time())
|
||||
r = getSecretInt().to_bytes(32, byteorder="big")
|
||||
if deterministic:
|
||||
assert smsg_timestamp is not None
|
||||
h = hashlib.sha256(b"smsg")
|
||||
h.update(privkey_from)
|
||||
h.update(pubkey_to)
|
||||
h.update(payload)
|
||||
h.update(smsg_timestamp.to_bytes(8, byteorder="big"))
|
||||
r = h.digest()
|
||||
smsg_iv: bytes = hashlib.sha256(b"smsg_iv" + r).digest()[:16]
|
||||
else:
|
||||
r = getSecretInt().to_bytes(32, byteorder="big")
|
||||
smsg_iv: bytes = secrets.token_bytes(16)
|
||||
if smsg_timestamp is None:
|
||||
smsg_timestamp = int(time.time())
|
||||
R = PublicKey.from_secret(r).format()
|
||||
p = PrivateKey(r).ecdh(pubkey_to)
|
||||
H = hashlib.sha512(p).digest()
|
||||
key_e: bytes = H[:32]
|
||||
key_m: bytes = H[32:]
|
||||
|
||||
smsg_iv: bytes = secrets.token_bytes(16)
|
||||
|
||||
payload_hash: bytes = sha256(sha256(payload))
|
||||
signature: bytes = PrivateKey(privkey_from).sign_recoverable(
|
||||
payload_hash, hasher=None
|
||||
|
||||
Reference in New Issue
Block a user