From 1e38fe695fd4b0077768c0eaae6d95d93ef5f10f Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sun, 18 Dec 2022 17:10:06 +0100 Subject: [PATCH 01/15] add nostr --- cashu/wallet/cli.py | 70 ++++++++++++++ nostr/__init__.py | 0 nostr/bech32.py | 137 +++++++++++++++++++++++++++ nostr/client/__init__.py | 0 nostr/client/aes.py | 86 +++++++++++++++++ nostr/client/cbc.py | 41 ++++++++ nostr/client/client.py | 165 +++++++++++++++++++++++++++++++++ nostr/event.py | 65 +++++++++++++ nostr/filter.py | 81 ++++++++++++++++ nostr/key.py | 86 +++++++++++++++++ nostr/message_pool.py | 78 ++++++++++++++++ nostr/message_type.py | 15 +++ nostr/nostr/__init__.py | 0 nostr/nostr/bech32.py | 137 +++++++++++++++++++++++++++ nostr/nostr/client/__init__.py | 0 nostr/nostr/client/aes.py | 86 +++++++++++++++++ nostr/nostr/client/cbc.py | 41 ++++++++ nostr/nostr/client/client.py | 165 +++++++++++++++++++++++++++++++++ nostr/nostr/event.py | 65 +++++++++++++ nostr/nostr/filter.py | 81 ++++++++++++++++ nostr/nostr/key.py | 86 +++++++++++++++++ nostr/nostr/message_pool.py | 78 ++++++++++++++++ nostr/nostr/message_type.py | 15 +++ nostr/nostr/relay.py | 123 ++++++++++++++++++++++++ nostr/nostr/relay_manager.py | 43 +++++++++ nostr/nostr/subscription.py | 12 +++ nostr/relay.py | 124 +++++++++++++++++++++++++ nostr/relay_manager.py | 43 +++++++++ nostr/subscription.py | 12 +++ poetry.lock | 102 +++++++++++++++++++- pyproject.toml | 3 + 31 files changed, 2039 insertions(+), 1 deletion(-) create mode 100644 nostr/__init__.py create mode 100644 nostr/bech32.py create mode 100644 nostr/client/__init__.py create mode 100644 nostr/client/aes.py create mode 100644 nostr/client/cbc.py create mode 100644 nostr/client/client.py create mode 100644 nostr/event.py create mode 100644 nostr/filter.py create mode 100644 nostr/key.py create mode 100644 nostr/message_pool.py create mode 100644 nostr/message_type.py create mode 100644 nostr/nostr/__init__.py create mode 100644 nostr/nostr/bech32.py create mode 100644 nostr/nostr/client/__init__.py create mode 100644 nostr/nostr/client/aes.py create mode 100644 nostr/nostr/client/cbc.py create mode 100644 nostr/nostr/client/client.py create mode 100644 nostr/nostr/event.py create mode 100644 nostr/nostr/filter.py create mode 100644 nostr/nostr/key.py create mode 100644 nostr/nostr/message_pool.py create mode 100644 nostr/nostr/message_type.py create mode 100644 nostr/nostr/relay.py create mode 100644 nostr/nostr/relay_manager.py create mode 100644 nostr/nostr/subscription.py create mode 100644 nostr/relay.py create mode 100644 nostr/relay_manager.py create mode 100644 nostr/subscription.py diff --git a/cashu/wallet/cli.py b/cashu/wallet/cli.py index 09ff7d9..d454181 100644 --- a/cashu/wallet/cli.py +++ b/cashu/wallet/cli.py @@ -39,6 +39,9 @@ from cashu.wallet.crud import ( ) from cashu.wallet.wallet import Wallet as Wallet +from nostr.client.client import NostrClient +from nostr.key import PublicKey + async def init_wallet(wallet: Wallet): """Performs migrations and loads proofs from db.""" @@ -433,3 +436,70 @@ async def info(ctx): print(f"Socks proxy: {SOCKS_HOST}:{SOCKS_PORT}") print(f"Mint URL: {MINT_URL}") return + + +@cli.command("nostr", help="Receive tokens via nostr.") +@click.pass_context +@coro +async def nostr(ctx): + wallet: Wallet = ctx.obj["WALLET"] + await wallet.load_mint() + client = NostrClient( + privatekey_hex="bfc6e7b0b998645d45aa451a3b9a3174bfe696fba78e86a86637a16f43e6c683" + ) + print(f"Your nostr public key: {client.public_key.hex()}") + await asyncio.sleep(2) + + def get_token_callback(token): + try: + proofs = [Proof(**p) for p in json.loads(base64.urlsafe_b64decode(token))] + wallet: Wallet = ctx.obj["WALLET"] + asyncio.run(wallet.redeem(proofs)) + wallet.status() + except Exception as e: + pass + + import threading + + t = threading.Thread( + target=client.get_dm, + args=( + client.public_key, + get_token_callback, + ), + name="Nostr DM", + ) + t.start() + + +@cli.command("nostrsend", help="Send tokens via nostr.") +@click.argument("amount", type=int) +@click.argument( + "pubkey", + type=str, + default="13395e6d975825cb811549b4b6ba6695c7ea8f75e1f3658d6cee2bee243195c3", +) +@click.pass_context +@coro +async def nostrsend(ctx, amount: int, pubkey: str): + wallet: Wallet = ctx.obj["WALLET"] + await wallet.load_mint() + wallet.status() + _, send_proofs = await wallet.split_to_send( + wallet.proofs, amount, set_reserved=True + ) + token = await wallet.serialize_proofs(send_proofs) + + from random import randrange + + # token = f"Token {randrange(1000)}" + print(token) + wallet.status() + + client = NostrClient( + privatekey_hex="bfc6e7b0b598645d45aa451a3b9a3174bfe696fba78e86a86637a16f4ee6d683" + ) + await asyncio.sleep(1) + client.dm(token, PublicKey(bytes.fromhex(pubkey))) + print("Sent") + client.close() diff --git a/nostr/__init__.py b/nostr/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/nostr/bech32.py b/nostr/bech32.py new file mode 100644 index 0000000..b068de7 --- /dev/null +++ b/nostr/bech32.py @@ -0,0 +1,137 @@ +# Copyright (c) 2017, 2020 Pieter Wuille +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +"""Reference implementation for Bech32/Bech32m and segwit addresses.""" + + +from enum import Enum + +class Encoding(Enum): + """Enumeration type to list the various supported encodings.""" + BECH32 = 1 + BECH32M = 2 + +CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" +BECH32M_CONST = 0x2bc830a3 + +def bech32_polymod(values): + """Internal function that computes the Bech32 checksum.""" + generator = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3] + chk = 1 + for value in values: + top = chk >> 25 + chk = (chk & 0x1ffffff) << 5 ^ value + for i in range(5): + chk ^= generator[i] if ((top >> i) & 1) else 0 + return chk + + +def bech32_hrp_expand(hrp): + """Expand the HRP into values for checksum computation.""" + return [ord(x) >> 5 for x in hrp] + [0] + [ord(x) & 31 for x in hrp] + + +def bech32_verify_checksum(hrp, data): + """Verify a checksum given HRP and converted data characters.""" + const = bech32_polymod(bech32_hrp_expand(hrp) + data) + if const == 1: + return Encoding.BECH32 + if const == BECH32M_CONST: + return Encoding.BECH32M + return None + +def bech32_create_checksum(hrp, data, spec): + """Compute the checksum values given HRP and data.""" + values = bech32_hrp_expand(hrp) + data + const = BECH32M_CONST if spec == Encoding.BECH32M else 1 + polymod = bech32_polymod(values + [0, 0, 0, 0, 0, 0]) ^ const + return [(polymod >> 5 * (5 - i)) & 31 for i in range(6)] + + +def bech32_encode(hrp, data, spec): + """Compute a Bech32 string given HRP and data values.""" + combined = data + bech32_create_checksum(hrp, data, spec) + return hrp + '1' + ''.join([CHARSET[d] for d in combined]) + +def bech32_decode(bech): + """Validate a Bech32/Bech32m string, and determine HRP and data.""" + if ((any(ord(x) < 33 or ord(x) > 126 for x in bech)) or + (bech.lower() != bech and bech.upper() != bech)): + return (None, None, None) + bech = bech.lower() + pos = bech.rfind('1') + if pos < 1 or pos + 7 > len(bech) or len(bech) > 90: + return (None, None, None) + if not all(x in CHARSET for x in bech[pos+1:]): + return (None, None, None) + hrp = bech[:pos] + data = [CHARSET.find(x) for x in bech[pos+1:]] + spec = bech32_verify_checksum(hrp, data) + if spec is None: + return (None, None, None) + return (hrp, data[:-6], spec) + +def convertbits(data, frombits, tobits, pad=True): + """General power-of-2 base conversion.""" + acc = 0 + bits = 0 + ret = [] + maxv = (1 << tobits) - 1 + max_acc = (1 << (frombits + tobits - 1)) - 1 + for value in data: + if value < 0 or (value >> frombits): + return None + acc = ((acc << frombits) | value) & max_acc + bits += frombits + while bits >= tobits: + bits -= tobits + ret.append((acc >> bits) & maxv) + if pad: + if bits: + ret.append((acc << (tobits - bits)) & maxv) + elif bits >= frombits or ((acc << (tobits - bits)) & maxv): + return None + return ret + + +def decode(hrp, addr): + """Decode a segwit address.""" + hrpgot, data, spec = bech32_decode(addr) + if hrpgot != hrp: + return (None, None) + decoded = convertbits(data[1:], 5, 8, False) + if decoded is None or len(decoded) < 2 or len(decoded) > 40: + return (None, None) + if data[0] > 16: + return (None, None) + if data[0] == 0 and len(decoded) != 20 and len(decoded) != 32: + return (None, None) + if data[0] == 0 and spec != Encoding.BECH32 or data[0] != 0 and spec != Encoding.BECH32M: + return (None, None) + return (data[0], decoded) + + +def encode(hrp, witver, witprog): + """Encode a segwit address.""" + spec = Encoding.BECH32 if witver == 0 else Encoding.BECH32M + ret = bech32_encode(hrp, [witver] + convertbits(witprog, 8, 5), spec) + if decode(hrp, ret) == (None, None): + return None + return ret diff --git a/nostr/client/__init__.py b/nostr/client/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/nostr/client/aes.py b/nostr/client/aes.py new file mode 100644 index 0000000..9d20224 --- /dev/null +++ b/nostr/client/aes.py @@ -0,0 +1,86 @@ +import base64 +import getpass +from hashlib import md5 + +from Cryptodome import Random +from Cryptodome.Cipher import AES + +BLOCK_SIZE = 16 + +import getpass + + +class AESCipher(object): + """This class is compatible with crypto-js/aes.js + + Encrypt and decrypt in Javascript using: + import AES from "crypto-js/aes.js"; + import Utf8 from "crypto-js/enc-utf8.js"; + AES.encrypt(decrypted, password).toString() + AES.decrypt(encrypted, password).toString(Utf8); + + """ + + def __init__(self, key=None, description=""): + self.key = key + self.description = description + " " + + def pad(self, data): + length = BLOCK_SIZE - (len(data) % BLOCK_SIZE) + return data + (chr(length) * length).encode() + + def unpad(self, data): + return data[: -(data[-1] if type(data[-1]) == int else ord(data[-1]))] + + @property + def passphrase(self): + passphrase = self.key if self.key is not None else None + if passphrase is None: + passphrase = getpass.getpass(f"Enter {self.description}password:") + return passphrase + + def bytes_to_key(self, data, salt, output=48): + # extended from https://gist.github.com/gsakkis/4546068 + assert len(salt) == 8, len(salt) + data += salt + key = md5(data).digest() + final_key = key + while len(final_key) < output: + key = md5(key + data).digest() + final_key += key + return final_key[:output] + + def decrypt(self, encrypted: str) -> str: # type: ignore + """Decrypts a string using AES-256-CBC.""" + passphrase = self.passphrase + encrypted = base64.b64decode(encrypted) # type: ignore + assert encrypted[0:8] == b"Salted__" + salt = encrypted[8:16] + key_iv = self.bytes_to_key(passphrase.encode(), salt, 32 + 16) + key = key_iv[:32] + iv = key_iv[32:] + aes = AES.new(key, AES.MODE_CBC, iv) + try: + return self.unpad(aes.decrypt(encrypted[16:])).decode() # type: ignore + except UnicodeDecodeError: + raise ValueError("Wrong passphrase") + + def encrypt(self, message: bytes) -> str: + passphrase = self.passphrase + salt = Random.new().read(8) + key_iv = self.bytes_to_key(passphrase.encode(), salt, 32 + 16) + key = key_iv[:32] + iv = key_iv[32:] + aes = AES.new(key, AES.MODE_CBC, iv) + return base64.b64encode( + b"Salted__" + salt + aes.encrypt(self.pad(message)) + ).decode() + + +# # if this file is executed directly, ask for a macaroon and encrypt it +# if __name__ == "__main__": +# macaroon = input("Enter macaroon: ") +# macaroon = load_macaroon(macaroon) +# macaroon = AESCipher(description="encryption").encrypt(macaroon.encode()) +# logger.info("Encrypted macaroon:") +# logger.info(macaroon) diff --git a/nostr/client/cbc.py b/nostr/client/cbc.py new file mode 100644 index 0000000..a41dbc0 --- /dev/null +++ b/nostr/client/cbc.py @@ -0,0 +1,41 @@ + +from Cryptodome import Random +from Cryptodome.Cipher import AES + +plain_text = "This is the text to encrypts" + +# encrypted = "7mH9jq3K9xNfWqIyu9gNpUz8qBvGwsrDJ+ACExdV1DvGgY8q39dkxVKeXD7LWCDrPnoD/ZFHJMRMis8v9lwHfNgJut8EVTMuJJi8oTgJevOBXl+E+bJPwej9hY3k20rgCQistNRtGHUzdWyOv7S1tg==".encode() +# iv = "GzDzqOVShWu3Pl2313FBpQ==".encode() + +key = bytes.fromhex("3aa925cb69eb613e2928f8a18279c78b1dca04541dfd064df2eda66b59880795") + +BLOCK_SIZE = 16 + +class AESCipher(object): + """This class is compatible with crypto.createCipheriv('aes-256-cbc') + + """ + def __init__(self, key=None): + self.key = key + + def pad(self, data): + length = BLOCK_SIZE - (len(data) % BLOCK_SIZE) + return data + (chr(length) * length).encode() + + def unpad(self, data): + return data[: -(data[-1] if type(data[-1]) == int else ord(data[-1]))] + + def encrypt(self, plain_text): + cipher = AES.new(self.key, AES.MODE_CBC) + b = plain_text.encode("UTF-8") + return cipher.iv, cipher.encrypt(self.pad(b)) + + def decrypt(self, iv, enc_text): + cipher = AES.new(self.key, AES.MODE_CBC, iv=iv) + return self.unpad(cipher.decrypt(enc_text).decode("UTF-8")) + +if __name__ == "__main__": + aes = AESCipher(key=key) + iv, enc_text = aes.encrypt(plain_text) + dec_text = aes.decrypt(iv, enc_text) + print(dec_text) \ No newline at end of file diff --git a/nostr/client/client.py b/nostr/client/client.py new file mode 100644 index 0000000..74aed9d --- /dev/null +++ b/nostr/client/client.py @@ -0,0 +1,165 @@ +from typing import * +import ssl +import time +import json +import os +import base64 + +from nostr.event import Event +from nostr.relay_manager import RelayManager +from nostr.message_type import ClientMessageType +from nostr.key import PrivateKey, PublicKey + +from nostr.filter import Filter, Filters +from nostr.event import Event, EventKind +from nostr.relay_manager import RelayManager +from nostr.message_type import ClientMessageType + +# from aes import AESCipher +from . import cbc + + +class NostrClient: + relays = [ + "wss://nostr.zebedee.cloud" + ] # ["wss://nostr.oxtr.dev"] # ["wss://relay.nostr.info"] "wss://nostr-pub.wellorder.net" "ws://91.237.88.218:2700", "wss://nostrrr.bublina.eu.org", ""wss://nostr-relay.freeberty.net"", , "wss://nostr.oxtr.dev", "wss://relay.nostr.info", "wss://nostr-pub.wellorder.net" , "wss://relayer.fiatjaf.com", "wss://nodestr.fmt.wiz.biz/" + relay_manager = RelayManager() + private_key: PrivateKey + public_key: PublicKey + + def __init__(self, privatekey_hex: str = "", relays: List[str] = []): + self.generate_keys(privatekey_hex) + + if len(relays): + self.relays = relays + + for relay in self.relays: + self.relay_manager.add_relay(relay) + self.relay_manager.open_connections( + {"cert_reqs": ssl.CERT_NONE} + ) # NOTE: This disables ssl certificate verification + + def close(self): + self.relay_manager.close_connections() + + def generate_keys(self, privatekey_hex: str = None): + pk = bytes.fromhex(privatekey_hex) if privatekey_hex else None + self.private_key = PrivateKey(pk) + self.public_key = self.private_key.public_key + print(f"Private key: {self.private_key.bech32()} ({self.private_key.hex()})") + print(f"Public key: {self.public_key.bech32()} ({self.public_key.hex()})") + + def post(self, message: str): + event = Event(self.public_key.hex(), message, kind=EventKind.TEXT_NOTE) + event.sign(self.private_key.hex()) + message = json.dumps([ClientMessageType.EVENT, event.to_json_object()]) + print("Publishing message:") + print(message) + self.relay_manager.publish_message(message) + + def get_post(self, sender_publickey: PublicKey): + filters = Filters( + [Filter(authors=[sender_publickey.hex()], kinds=[EventKind.TEXT_NOTE])] + ) + subscription_id = os.urandom(4).hex() + self.relay_manager.add_subscription(subscription_id, filters) + + request = [ClientMessageType.REQUEST, subscription_id] + request.extend(filters.to_json_array()) + message = json.dumps(request) + print("Subscribing to events:") + print(message) + self.relay_manager.publish_message(message) + + message_received = False + while True: + while self.relay_manager.message_pool.has_events(): + event_msg = self.relay_manager.message_pool.get_event() + print(event_msg.event.content) + message_received = True + break + else: + time.sleep(0.1) + + def dm(self, message: str, to_pubkey: PublicKey): + + shared_secret = self.private_key.compute_shared_secret( + to_pubkey.hex() + ) + + # print("shared secret: ", shared_secret.hex()) + # print("plain text:", message) + aes = cbc.AESCipher(key=shared_secret) + iv, enc_text = aes.encrypt(message) + # print("encrypt iv: ", iv) + content = f"{base64.b64encode(enc_text).decode('utf-8')}?iv={base64.b64encode(iv).decode('utf-8')}" + + + event = Event( + self.public_key.hex(), + content, + tags=[["p", to_pubkey.hex()]], + kind=EventKind.ENCRYPTED_DIRECT_MESSAGE, + ) + event.sign(self.private_key.hex()) + event_message = json.dumps([ClientMessageType.EVENT, event.to_json_object()]) + # print("DM message:") + # print(event_message) + + time.sleep(1) + self.relay_manager.publish_message(event_message) + + def get_dm(self, sender_publickey: PublicKey, callback_func=None): + filters = Filters( + [ + Filter( + kinds=[EventKind.ENCRYPTED_DIRECT_MESSAGE], + tags={"#p": [sender_publickey.hex()]}, + ) + ] + ) + subscription_id = os.urandom(4).hex() + self.relay_manager.add_subscription(subscription_id, filters) + + request = [ClientMessageType.REQUEST, subscription_id] + request.extend(filters.to_json_array()) + message = json.dumps(request) + # print("Subscribing to events:") + # print(message) + self.relay_manager.publish_message(message) + + while True: + while self.relay_manager.message_pool.has_events(): + event_msg = self.relay_manager.message_pool.get_event() + + if "?iv=" in event_msg.event.content: + try: + shared_secret = self.private_key.compute_shared_secret( + event_msg.event.public_key + ) + # print("shared secret: ", shared_secret.hex()) + # print("plain text:", message) + aes = cbc.AESCipher(key=shared_secret) + enc_text_b64, iv_b64 = event_msg.event.content.split("?iv=") + iv = base64.decodebytes(iv_b64.encode("utf-8")) + enc_text = base64.decodebytes(enc_text_b64.encode("utf-8")) + # print("decrypt iv: ", iv) + dec_text = aes.decrypt(iv, enc_text) + print(f"From {event_msg.event.public_key[:5]}...: {dec_text}") + if callback_func: + callback_func(dec_text) + except: + pass + else: + print(f"\nFrom {event_msg.event.public_key[:5]}...: {event_msg.event.content}") + break + time.sleep(0.1) + + async def subscribe(self): + while True: + while self.relay_manager.message_pool.has_events(): + event_msg = self.relay_manager.message_pool.get_event() + print(event_msg.event.content) + break + time.sleep(0.1) + diff --git a/nostr/event.py b/nostr/event.py new file mode 100644 index 0000000..450893e --- /dev/null +++ b/nostr/event.py @@ -0,0 +1,65 @@ +import time +import json +from enum import IntEnum +from secp256k1 import PrivateKey, PublicKey +from hashlib import sha256 + +class EventKind(IntEnum): + SET_METADATA = 0 + TEXT_NOTE = 1 + RECOMMEND_RELAY = 2 + CONTACTS = 3 + ENCRYPTED_DIRECT_MESSAGE = 4 + DELETE = 5 + +class Event(): + def __init__( + self, + public_key: str, + content: str, + created_at: int=int(time.time()), + kind: int=EventKind.TEXT_NOTE, + tags: "list[list[str]]"=[], + id: str=None, + signature: str=None) -> None: + if not isinstance(content, str): + raise TypeError("Argument 'content' must be of type str") + + self.id = id if not id is None else Event.compute_id(public_key, created_at, kind, tags, content) + self.public_key = public_key + self.content = content + self.created_at = created_at + self.kind = kind + self.tags = tags + self.signature = signature + + @staticmethod + def serialize(public_key: str, created_at: int, kind: int, tags: "list[list[str]]", content: str) -> bytes: + data = [0, public_key, created_at, kind, tags, content] + data_str = json.dumps(data, separators=(',', ':'), ensure_ascii=False) + return data_str.encode() + + @staticmethod + def compute_id(public_key: str, created_at: int, kind: int, tags: "list[list[str]]", content: str) -> str: + return sha256(Event.serialize(public_key, created_at, kind, tags, content)).hexdigest() + + def sign(self, private_key_hex: str) -> None: + sk = PrivateKey(bytes.fromhex(private_key_hex)) + sig = sk.schnorr_sign(bytes.fromhex(self.id), None, raw=True) + self.signature = sig.hex() + + def verify(self) -> bool: + pub_key = PublicKey(bytes.fromhex("02" + self.public_key), True) # add 02 for schnorr (bip340) + event_id = Event.compute_id(self.public_key, self.created_at, self.kind, self.tags, self.content) + return pub_key.schnorr_verify(bytes.fromhex(event_id), bytes.fromhex(self.signature), None, raw=True) + + def to_json_object(self) -> dict: + return { + "id": self.id, + "pubkey": self.public_key, + "created_at": self.created_at, + "kind": self.kind, + "tags": self.tags, + "content": self.content, + "sig": self.signature + } diff --git a/nostr/filter.py b/nostr/filter.py new file mode 100644 index 0000000..32b94a3 --- /dev/null +++ b/nostr/filter.py @@ -0,0 +1,81 @@ +from collections import UserList +from .event import Event + + +class Filter: + def __init__( + self, + ids: "list[str]" = None, + kinds: "list[int]" = None, + authors: "list[str]" = None, + since: int = None, + until: int = None, + tags: "dict[str, list[str]]" = None, + limit: int = None, + ) -> None: + self.IDs = ids + self.kinds = kinds + self.authors = authors + self.since = since + self.until = until + self.tags = tags + self.limit = limit + + def matches(self, event: Event) -> bool: + if self.IDs != None and event.id not in self.IDs: + return False + if self.kinds != None and event.kind not in self.kinds: + return False + if self.authors != None and event.public_key not in self.authors: + return False + if self.since != None and event.created_at < self.since: + return False + if self.until != None and event.created_at > self.until: + return False + if self.tags != None and len(event.tags) == 0: + return False + if self.tags != None: + e_tag_identifiers = [e_tag[0] for e_tag in event.tags] + for f_tag, f_tag_values in self.tags.items(): + if f_tag[1:] not in e_tag_identifiers: + return False + for e_tag in event.tags: + if e_tag[1] not in f_tag_values: + return False + + return True + + def to_json_object(self) -> dict: + res = {} + if self.IDs != None: + res["ids"] = self.IDs + if self.kinds != None: + res["kinds"] = self.kinds + if self.authors != None: + res["authors"] = self.authors + if self.since != None: + res["since"] = self.since + if self.until != None: + res["until"] = self.until + if self.tags != None: + for tag, values in self.tags.items(): + res[tag] = values + if self.limit != None: + res["limit"] = self.limit + + return res + + +class Filters(UserList): + def __init__(self, initlist: "list[Filter]" = []) -> None: + super().__init__(initlist) + self.data: "list[Filter]" + + def match(self, event: Event): + for filter in self.data: + if filter.matches(event): + return True + return False + + def to_json_array(self) -> list: + return [filter.to_json_object() for filter in self.data] diff --git a/nostr/key.py b/nostr/key.py new file mode 100644 index 0000000..9449c00 --- /dev/null +++ b/nostr/key.py @@ -0,0 +1,86 @@ +import secrets +import base64 +import secp256k1 +from cffi import FFI +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes +from cryptography.hazmat.primitives import padding +from . import bech32 + +class PublicKey: + def __init__(self, raw_bytes: bytes) -> None: + self.raw_bytes = raw_bytes + + def bech32(self) -> str: + converted_bits = bech32.convertbits(self.raw_bytes, 8, 5) + return bech32.bech32_encode("npub", converted_bits, bech32.Encoding.BECH32) + + def hex(self) -> str: + return self.raw_bytes.hex() + + def verify_signed_message_hash(self, hash: str, sig: str) -> bool: + pk = secp256k1.PublicKey(b"\x02" + self.raw_bytes, True) + return pk.schnorr_verify(bytes.fromhex(hash), bytes.fromhex(sig), None, True) + +class PrivateKey: + def __init__(self, raw_secret: bytes=None) -> None: + if not raw_secret is None: + self.raw_secret = raw_secret + else: + self.raw_secret = secrets.token_bytes(32) + + sk = secp256k1.PrivateKey(self.raw_secret) + self.public_key = PublicKey(sk.pubkey.serialize()[1:]) + + def bech32(self) -> str: + converted_bits = bech32.convertbits(self.raw_secret, 8, 5) + return bech32.bech32_encode("nsec", converted_bits, bech32.Encoding.BECH32) + + def hex(self) -> str: + return self.raw_secret.hex() + + def tweak_add(self, scalar: bytes) -> bytes: + sk = secp256k1.PrivateKey(self.raw_secret) + return sk.tweak_add(scalar) + + def compute_shared_secret(self, public_key_hex: str) -> bytes: + pk = secp256k1.PublicKey(bytes.fromhex("02" + public_key_hex), True) + return pk.ecdh(self.raw_secret, hashfn=copy_x) + + def encrypt_message(self, message: str, public_key_hex: str) -> str: + padder = padding.PKCS7(128).padder() + padded_data = padder.update(message.encode()) + padder.finalize() + + iv = secrets.token_bytes(16) + cipher = Cipher(algorithms.AES(self.compute_shared_secret(public_key_hex)), modes.CBC(iv)) + + encryptor = cipher.encryptor() + encrypted_message = encryptor.update(padded_data) + encryptor.finalize() + + return f"{base64.b64encode(encrypted_message).decode()}?iv={base64.b64encode(iv).decode()}" + + def decrypt_message(self, encoded_message: str, public_key_hex: str) -> str: + encoded_data = encoded_message.split('?iv=') + encoded_content, encoded_iv = encoded_data[0], encoded_data[1] + + iv = base64.b64decode(encoded_iv) + cipher = Cipher(algorithms.AES(self.compute_shared_secret(public_key_hex)), modes.CBC(iv)) + encrypted_content = base64.b64decode(encoded_content) + + decryptor = cipher.decryptor() + decrypted_message = decryptor.update(encrypted_content) + decryptor.finalize() + + unpadder = padding.PKCS7(128).unpadder() + unpadded_data = unpadder.update(decrypted_message) + unpadder.finalize() + + return unpadded_data.decode() + + def sign_message_hash(self, hash: bytes) -> str: + sk = secp256k1.PrivateKey(self.raw_secret) + sig = sk.schnorr_sign(hash, None, raw=True) + return sig.hex() + +ffi = FFI() +@ffi.callback("int (unsigned char *, const unsigned char *, const unsigned char *, void *)") +def copy_x(output, x32, y32, data): + ffi.memmove(output, x32, 32) + return 1 \ No newline at end of file diff --git a/nostr/message_pool.py b/nostr/message_pool.py new file mode 100644 index 0000000..472e31e --- /dev/null +++ b/nostr/message_pool.py @@ -0,0 +1,78 @@ +import json +from queue import Queue +from threading import Lock +from .message_type import RelayMessageType +from .event import Event + + +class EventMessage: + def __init__(self, event: Event, subscription_id: str, url: str) -> None: + self.event = event + self.subscription_id = subscription_id + self.url = url + + +class NoticeMessage: + def __init__(self, content: str, url: str) -> None: + self.content = content + self.url = url + + +class EndOfStoredEventsMessage: + def __init__(self, subscription_id: str, url: str) -> None: + self.subscription_id = subscription_id + self.url = url + + +class MessagePool: + def __init__(self) -> None: + self.events: Queue[EventMessage] = Queue() + self.notices: Queue[NoticeMessage] = Queue() + self.eose_notices: Queue[EndOfStoredEventsMessage] = Queue() + self._unique_events: set = set() + self.lock: Lock = Lock() + + def add_message(self, message: str, url: str): + self._process_message(message, url) + + def get_event(self): + return self.events.get() + + def get_notice(self): + return self.notices.get() + + def get_eose_notice(self): + return self.eose_notices.get() + + def has_events(self): + return self.events.qsize() > 0 + + def has_notices(self): + return self.notices.qsize() > 0 + + def has_eose_notices(self): + return self.eose_notices.qsize() > 0 + + def _process_message(self, message: str, url: str): + message_json = json.loads(message) + message_type = message_json[0] + if message_type == RelayMessageType.EVENT: + subscription_id = message_json[1] + e = message_json[2] + event = Event( + e["pubkey"], + e["content"], + e["created_at"], + e["kind"], + e["tags"], + e["id"], + e["sig"], + ) + with self.lock: + if not event.id in self._unique_events: + self.events.put(EventMessage(event, subscription_id, url)) + self._unique_events.add(event.id) + elif message_type == RelayMessageType.NOTICE: + self.notices.put(NoticeMessage(message_json[1], url)) + elif message_type == RelayMessageType.END_OF_STORED_EVENTS: + self.eose_notices.put(EndOfStoredEventsMessage(message_json[1], url)) diff --git a/nostr/message_type.py b/nostr/message_type.py new file mode 100644 index 0000000..3f5206b --- /dev/null +++ b/nostr/message_type.py @@ -0,0 +1,15 @@ +class ClientMessageType: + EVENT = "EVENT" + REQUEST = "REQ" + CLOSE = "CLOSE" + +class RelayMessageType: + EVENT = "EVENT" + NOTICE = "NOTICE" + END_OF_STORED_EVENTS = "EOSE" + + @staticmethod + def is_valid(type: str) -> bool: + if type == RelayMessageType.EVENT or type == RelayMessageType.NOTICE or type == RelayMessageType.END_OF_STORED_EVENTS: + return True + return False \ No newline at end of file diff --git a/nostr/nostr/__init__.py b/nostr/nostr/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/nostr/nostr/bech32.py b/nostr/nostr/bech32.py new file mode 100644 index 0000000..b068de7 --- /dev/null +++ b/nostr/nostr/bech32.py @@ -0,0 +1,137 @@ +# Copyright (c) 2017, 2020 Pieter Wuille +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +"""Reference implementation for Bech32/Bech32m and segwit addresses.""" + + +from enum import Enum + +class Encoding(Enum): + """Enumeration type to list the various supported encodings.""" + BECH32 = 1 + BECH32M = 2 + +CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" +BECH32M_CONST = 0x2bc830a3 + +def bech32_polymod(values): + """Internal function that computes the Bech32 checksum.""" + generator = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3] + chk = 1 + for value in values: + top = chk >> 25 + chk = (chk & 0x1ffffff) << 5 ^ value + for i in range(5): + chk ^= generator[i] if ((top >> i) & 1) else 0 + return chk + + +def bech32_hrp_expand(hrp): + """Expand the HRP into values for checksum computation.""" + return [ord(x) >> 5 for x in hrp] + [0] + [ord(x) & 31 for x in hrp] + + +def bech32_verify_checksum(hrp, data): + """Verify a checksum given HRP and converted data characters.""" + const = bech32_polymod(bech32_hrp_expand(hrp) + data) + if const == 1: + return Encoding.BECH32 + if const == BECH32M_CONST: + return Encoding.BECH32M + return None + +def bech32_create_checksum(hrp, data, spec): + """Compute the checksum values given HRP and data.""" + values = bech32_hrp_expand(hrp) + data + const = BECH32M_CONST if spec == Encoding.BECH32M else 1 + polymod = bech32_polymod(values + [0, 0, 0, 0, 0, 0]) ^ const + return [(polymod >> 5 * (5 - i)) & 31 for i in range(6)] + + +def bech32_encode(hrp, data, spec): + """Compute a Bech32 string given HRP and data values.""" + combined = data + bech32_create_checksum(hrp, data, spec) + return hrp + '1' + ''.join([CHARSET[d] for d in combined]) + +def bech32_decode(bech): + """Validate a Bech32/Bech32m string, and determine HRP and data.""" + if ((any(ord(x) < 33 or ord(x) > 126 for x in bech)) or + (bech.lower() != bech and bech.upper() != bech)): + return (None, None, None) + bech = bech.lower() + pos = bech.rfind('1') + if pos < 1 or pos + 7 > len(bech) or len(bech) > 90: + return (None, None, None) + if not all(x in CHARSET for x in bech[pos+1:]): + return (None, None, None) + hrp = bech[:pos] + data = [CHARSET.find(x) for x in bech[pos+1:]] + spec = bech32_verify_checksum(hrp, data) + if spec is None: + return (None, None, None) + return (hrp, data[:-6], spec) + +def convertbits(data, frombits, tobits, pad=True): + """General power-of-2 base conversion.""" + acc = 0 + bits = 0 + ret = [] + maxv = (1 << tobits) - 1 + max_acc = (1 << (frombits + tobits - 1)) - 1 + for value in data: + if value < 0 or (value >> frombits): + return None + acc = ((acc << frombits) | value) & max_acc + bits += frombits + while bits >= tobits: + bits -= tobits + ret.append((acc >> bits) & maxv) + if pad: + if bits: + ret.append((acc << (tobits - bits)) & maxv) + elif bits >= frombits or ((acc << (tobits - bits)) & maxv): + return None + return ret + + +def decode(hrp, addr): + """Decode a segwit address.""" + hrpgot, data, spec = bech32_decode(addr) + if hrpgot != hrp: + return (None, None) + decoded = convertbits(data[1:], 5, 8, False) + if decoded is None or len(decoded) < 2 or len(decoded) > 40: + return (None, None) + if data[0] > 16: + return (None, None) + if data[0] == 0 and len(decoded) != 20 and len(decoded) != 32: + return (None, None) + if data[0] == 0 and spec != Encoding.BECH32 or data[0] != 0 and spec != Encoding.BECH32M: + return (None, None) + return (data[0], decoded) + + +def encode(hrp, witver, witprog): + """Encode a segwit address.""" + spec = Encoding.BECH32 if witver == 0 else Encoding.BECH32M + ret = bech32_encode(hrp, [witver] + convertbits(witprog, 8, 5), spec) + if decode(hrp, ret) == (None, None): + return None + return ret diff --git a/nostr/nostr/client/__init__.py b/nostr/nostr/client/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/nostr/nostr/client/aes.py b/nostr/nostr/client/aes.py new file mode 100644 index 0000000..9d20224 --- /dev/null +++ b/nostr/nostr/client/aes.py @@ -0,0 +1,86 @@ +import base64 +import getpass +from hashlib import md5 + +from Cryptodome import Random +from Cryptodome.Cipher import AES + +BLOCK_SIZE = 16 + +import getpass + + +class AESCipher(object): + """This class is compatible with crypto-js/aes.js + + Encrypt and decrypt in Javascript using: + import AES from "crypto-js/aes.js"; + import Utf8 from "crypto-js/enc-utf8.js"; + AES.encrypt(decrypted, password).toString() + AES.decrypt(encrypted, password).toString(Utf8); + + """ + + def __init__(self, key=None, description=""): + self.key = key + self.description = description + " " + + def pad(self, data): + length = BLOCK_SIZE - (len(data) % BLOCK_SIZE) + return data + (chr(length) * length).encode() + + def unpad(self, data): + return data[: -(data[-1] if type(data[-1]) == int else ord(data[-1]))] + + @property + def passphrase(self): + passphrase = self.key if self.key is not None else None + if passphrase is None: + passphrase = getpass.getpass(f"Enter {self.description}password:") + return passphrase + + def bytes_to_key(self, data, salt, output=48): + # extended from https://gist.github.com/gsakkis/4546068 + assert len(salt) == 8, len(salt) + data += salt + key = md5(data).digest() + final_key = key + while len(final_key) < output: + key = md5(key + data).digest() + final_key += key + return final_key[:output] + + def decrypt(self, encrypted: str) -> str: # type: ignore + """Decrypts a string using AES-256-CBC.""" + passphrase = self.passphrase + encrypted = base64.b64decode(encrypted) # type: ignore + assert encrypted[0:8] == b"Salted__" + salt = encrypted[8:16] + key_iv = self.bytes_to_key(passphrase.encode(), salt, 32 + 16) + key = key_iv[:32] + iv = key_iv[32:] + aes = AES.new(key, AES.MODE_CBC, iv) + try: + return self.unpad(aes.decrypt(encrypted[16:])).decode() # type: ignore + except UnicodeDecodeError: + raise ValueError("Wrong passphrase") + + def encrypt(self, message: bytes) -> str: + passphrase = self.passphrase + salt = Random.new().read(8) + key_iv = self.bytes_to_key(passphrase.encode(), salt, 32 + 16) + key = key_iv[:32] + iv = key_iv[32:] + aes = AES.new(key, AES.MODE_CBC, iv) + return base64.b64encode( + b"Salted__" + salt + aes.encrypt(self.pad(message)) + ).decode() + + +# # if this file is executed directly, ask for a macaroon and encrypt it +# if __name__ == "__main__": +# macaroon = input("Enter macaroon: ") +# macaroon = load_macaroon(macaroon) +# macaroon = AESCipher(description="encryption").encrypt(macaroon.encode()) +# logger.info("Encrypted macaroon:") +# logger.info(macaroon) diff --git a/nostr/nostr/client/cbc.py b/nostr/nostr/client/cbc.py new file mode 100644 index 0000000..a41dbc0 --- /dev/null +++ b/nostr/nostr/client/cbc.py @@ -0,0 +1,41 @@ + +from Cryptodome import Random +from Cryptodome.Cipher import AES + +plain_text = "This is the text to encrypts" + +# encrypted = "7mH9jq3K9xNfWqIyu9gNpUz8qBvGwsrDJ+ACExdV1DvGgY8q39dkxVKeXD7LWCDrPnoD/ZFHJMRMis8v9lwHfNgJut8EVTMuJJi8oTgJevOBXl+E+bJPwej9hY3k20rgCQistNRtGHUzdWyOv7S1tg==".encode() +# iv = "GzDzqOVShWu3Pl2313FBpQ==".encode() + +key = bytes.fromhex("3aa925cb69eb613e2928f8a18279c78b1dca04541dfd064df2eda66b59880795") + +BLOCK_SIZE = 16 + +class AESCipher(object): + """This class is compatible with crypto.createCipheriv('aes-256-cbc') + + """ + def __init__(self, key=None): + self.key = key + + def pad(self, data): + length = BLOCK_SIZE - (len(data) % BLOCK_SIZE) + return data + (chr(length) * length).encode() + + def unpad(self, data): + return data[: -(data[-1] if type(data[-1]) == int else ord(data[-1]))] + + def encrypt(self, plain_text): + cipher = AES.new(self.key, AES.MODE_CBC) + b = plain_text.encode("UTF-8") + return cipher.iv, cipher.encrypt(self.pad(b)) + + def decrypt(self, iv, enc_text): + cipher = AES.new(self.key, AES.MODE_CBC, iv=iv) + return self.unpad(cipher.decrypt(enc_text).decode("UTF-8")) + +if __name__ == "__main__": + aes = AESCipher(key=key) + iv, enc_text = aes.encrypt(plain_text) + dec_text = aes.decrypt(iv, enc_text) + print(dec_text) \ No newline at end of file diff --git a/nostr/nostr/client/client.py b/nostr/nostr/client/client.py new file mode 100644 index 0000000..341778e --- /dev/null +++ b/nostr/nostr/client/client.py @@ -0,0 +1,165 @@ +from typing import * +import ssl +import time +import json +import os +import base64 + +from nostr.event import Event +from nostr.relay_manager import RelayManager +from nostr.message_type import ClientMessageType +from nostr.key import PrivateKey, PublicKey + +from nostr.filter import Filter, Filters +from nostr.event import Event, EventKind +from nostr.relay_manager import RelayManager +from nostr.message_type import ClientMessageType + +# from aes import AESCipher +from . import cbc + + +class NostrClient: + relays = [ + "wss://nostr.zebedee.cloud" + ] # ["wss://nostr.oxtr.dev"] # ["wss://relay.nostr.info"] "wss://nostr-pub.wellorder.net" "ws://91.237.88.218:2700", "wss://nostrrr.bublina.eu.org", ""wss://nostr-relay.freeberty.net"", , "wss://nostr.oxtr.dev", "wss://relay.nostr.info", "wss://nostr-pub.wellorder.net" , "wss://relayer.fiatjaf.com", "wss://nodestr.fmt.wiz.biz/", "wss://no.str.cr" + relay_manager = RelayManager() + private_key: PrivateKey + public_key: PublicKey + + def __init__(self, privatekey_hex: str = "", relays: List[str] = []): + self.generate_keys(privatekey_hex) + + if len(relays): + self.relays = relays + + for relay in self.relays: + self.relay_manager.add_relay(relay) + self.relay_manager.open_connections( + {"cert_reqs": ssl.CERT_NONE} + ) # NOTE: This disables ssl certificate verification + + def close(self): + self.relay_manager.close_connections() + + def generate_keys(self, privatekey_hex: str = None): + pk = bytes.fromhex(privatekey_hex) if privatekey_hex else None + self.private_key = PrivateKey(pk) + self.public_key = self.private_key.public_key + print(f"Private key: {self.private_key.bech32()} ({self.private_key.hex()})") + print(f"Public key: {self.public_key.bech32()} ({self.public_key.hex()})") + + def post(self, message: str): + event = Event(self.public_key.hex(), message, kind=EventKind.TEXT_NOTE) + event.sign(self.private_key.hex()) + message = json.dumps([ClientMessageType.EVENT, event.to_json_object()]) + # print("Publishing message:") + # print(message) + self.relay_manager.publish_message(message) + + def get_post(self, sender_publickey: PublicKey): + filters = Filters( + [Filter(authors=[sender_publickey.hex()], kinds=[EventKind.TEXT_NOTE])] + ) + subscription_id = os.urandom(4).hex() + self.relay_manager.add_subscription(subscription_id, filters) + + request = [ClientMessageType.REQUEST, subscription_id] + request.extend(filters.to_json_array()) + message = json.dumps(request) + # print("Subscribing to events:") + # print(message) + self.relay_manager.publish_message(message) + + message_received = False + while True: + while self.relay_manager.message_pool.has_events(): + event_msg = self.relay_manager.message_pool.get_event() + print(event_msg.event.content) + message_received = True + break + else: + time.sleep(0.1) + + def dm(self, message: str, to_pubkey: PublicKey): + + shared_secret = self.private_key.compute_shared_secret( + to_pubkey.hex() + ) + + # print("shared secret: ", shared_secret.hex()) + # print("plain text:", message) + aes = cbc.AESCipher(key=shared_secret) + iv, enc_text = aes.encrypt(message) + # print("encrypt iv: ", iv) + content = f"{base64.b64encode(enc_text).decode('utf-8')}?iv={base64.b64encode(iv).decode('utf-8')}" + + + event = Event( + self.public_key.hex(), + content, + tags=[["p", to_pubkey.hex()]], + kind=EventKind.ENCRYPTED_DIRECT_MESSAGE, + ) + event.sign(self.private_key.hex()) + event_message = json.dumps([ClientMessageType.EVENT, event.to_json_object()]) + # print("DM message:") + # print(event_message) + + time.sleep(1) + self.relay_manager.publish_message(event_message) + + def get_dm(self, sender_publickey: PublicKey, callback_func=None): + filters = Filters( + [ + Filter( + kinds=[EventKind.ENCRYPTED_DIRECT_MESSAGE], + tags={"#p": [sender_publickey.hex()]}, + ) + ] + ) + subscription_id = os.urandom(4).hex() + self.relay_manager.add_subscription(subscription_id, filters) + + request = [ClientMessageType.REQUEST, subscription_id] + request.extend(filters.to_json_array()) + message = json.dumps(request) + # print("Subscribing to events:") + # print(message) + self.relay_manager.publish_message(message) + + while True: + while self.relay_manager.message_pool.has_events(): + event_msg = self.relay_manager.message_pool.get_event() + + if "?iv=" in event_msg.event.content: + try: + shared_secret = self.private_key.compute_shared_secret( + event_msg.event.public_key + ) + # print("shared secret: ", shared_secret.hex()) + # print("plain text:", message) + aes = cbc.AESCipher(key=shared_secret) + enc_text_b64, iv_b64 = event_msg.event.content.split("?iv=") + iv = base64.decodebytes(iv_b64.encode("utf-8")) + enc_text = base64.decodebytes(enc_text_b64.encode("utf-8")) + # print("decrypt iv: ", iv) + dec_text = aes.decrypt(iv, enc_text) + # print(f"From {event_msg.event.public_key[:5]}...: {dec_text}") + if callback_func: + callback_func(event_msg.event, dec_text) + except: + pass + # else: + # print(f"\nFrom {event_msg.event.public_key[:5]}...: {event_msg.event.content}") + break + time.sleep(0.1) + + async def subscribe(self): + while True: + while self.relay_manager.message_pool.has_events(): + event_msg = self.relay_manager.message_pool.get_event() + print(event_msg.event.content) + break + time.sleep(0.1) + diff --git a/nostr/nostr/event.py b/nostr/nostr/event.py new file mode 100644 index 0000000..450893e --- /dev/null +++ b/nostr/nostr/event.py @@ -0,0 +1,65 @@ +import time +import json +from enum import IntEnum +from secp256k1 import PrivateKey, PublicKey +from hashlib import sha256 + +class EventKind(IntEnum): + SET_METADATA = 0 + TEXT_NOTE = 1 + RECOMMEND_RELAY = 2 + CONTACTS = 3 + ENCRYPTED_DIRECT_MESSAGE = 4 + DELETE = 5 + +class Event(): + def __init__( + self, + public_key: str, + content: str, + created_at: int=int(time.time()), + kind: int=EventKind.TEXT_NOTE, + tags: "list[list[str]]"=[], + id: str=None, + signature: str=None) -> None: + if not isinstance(content, str): + raise TypeError("Argument 'content' must be of type str") + + self.id = id if not id is None else Event.compute_id(public_key, created_at, kind, tags, content) + self.public_key = public_key + self.content = content + self.created_at = created_at + self.kind = kind + self.tags = tags + self.signature = signature + + @staticmethod + def serialize(public_key: str, created_at: int, kind: int, tags: "list[list[str]]", content: str) -> bytes: + data = [0, public_key, created_at, kind, tags, content] + data_str = json.dumps(data, separators=(',', ':'), ensure_ascii=False) + return data_str.encode() + + @staticmethod + def compute_id(public_key: str, created_at: int, kind: int, tags: "list[list[str]]", content: str) -> str: + return sha256(Event.serialize(public_key, created_at, kind, tags, content)).hexdigest() + + def sign(self, private_key_hex: str) -> None: + sk = PrivateKey(bytes.fromhex(private_key_hex)) + sig = sk.schnorr_sign(bytes.fromhex(self.id), None, raw=True) + self.signature = sig.hex() + + def verify(self) -> bool: + pub_key = PublicKey(bytes.fromhex("02" + self.public_key), True) # add 02 for schnorr (bip340) + event_id = Event.compute_id(self.public_key, self.created_at, self.kind, self.tags, self.content) + return pub_key.schnorr_verify(bytes.fromhex(event_id), bytes.fromhex(self.signature), None, raw=True) + + def to_json_object(self) -> dict: + return { + "id": self.id, + "pubkey": self.public_key, + "created_at": self.created_at, + "kind": self.kind, + "tags": self.tags, + "content": self.content, + "sig": self.signature + } diff --git a/nostr/nostr/filter.py b/nostr/nostr/filter.py new file mode 100644 index 0000000..32b94a3 --- /dev/null +++ b/nostr/nostr/filter.py @@ -0,0 +1,81 @@ +from collections import UserList +from .event import Event + + +class Filter: + def __init__( + self, + ids: "list[str]" = None, + kinds: "list[int]" = None, + authors: "list[str]" = None, + since: int = None, + until: int = None, + tags: "dict[str, list[str]]" = None, + limit: int = None, + ) -> None: + self.IDs = ids + self.kinds = kinds + self.authors = authors + self.since = since + self.until = until + self.tags = tags + self.limit = limit + + def matches(self, event: Event) -> bool: + if self.IDs != None and event.id not in self.IDs: + return False + if self.kinds != None and event.kind not in self.kinds: + return False + if self.authors != None and event.public_key not in self.authors: + return False + if self.since != None and event.created_at < self.since: + return False + if self.until != None and event.created_at > self.until: + return False + if self.tags != None and len(event.tags) == 0: + return False + if self.tags != None: + e_tag_identifiers = [e_tag[0] for e_tag in event.tags] + for f_tag, f_tag_values in self.tags.items(): + if f_tag[1:] not in e_tag_identifiers: + return False + for e_tag in event.tags: + if e_tag[1] not in f_tag_values: + return False + + return True + + def to_json_object(self) -> dict: + res = {} + if self.IDs != None: + res["ids"] = self.IDs + if self.kinds != None: + res["kinds"] = self.kinds + if self.authors != None: + res["authors"] = self.authors + if self.since != None: + res["since"] = self.since + if self.until != None: + res["until"] = self.until + if self.tags != None: + for tag, values in self.tags.items(): + res[tag] = values + if self.limit != None: + res["limit"] = self.limit + + return res + + +class Filters(UserList): + def __init__(self, initlist: "list[Filter]" = []) -> None: + super().__init__(initlist) + self.data: "list[Filter]" + + def match(self, event: Event): + for filter in self.data: + if filter.matches(event): + return True + return False + + def to_json_array(self) -> list: + return [filter.to_json_object() for filter in self.data] diff --git a/nostr/nostr/key.py b/nostr/nostr/key.py new file mode 100644 index 0000000..9449c00 --- /dev/null +++ b/nostr/nostr/key.py @@ -0,0 +1,86 @@ +import secrets +import base64 +import secp256k1 +from cffi import FFI +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes +from cryptography.hazmat.primitives import padding +from . import bech32 + +class PublicKey: + def __init__(self, raw_bytes: bytes) -> None: + self.raw_bytes = raw_bytes + + def bech32(self) -> str: + converted_bits = bech32.convertbits(self.raw_bytes, 8, 5) + return bech32.bech32_encode("npub", converted_bits, bech32.Encoding.BECH32) + + def hex(self) -> str: + return self.raw_bytes.hex() + + def verify_signed_message_hash(self, hash: str, sig: str) -> bool: + pk = secp256k1.PublicKey(b"\x02" + self.raw_bytes, True) + return pk.schnorr_verify(bytes.fromhex(hash), bytes.fromhex(sig), None, True) + +class PrivateKey: + def __init__(self, raw_secret: bytes=None) -> None: + if not raw_secret is None: + self.raw_secret = raw_secret + else: + self.raw_secret = secrets.token_bytes(32) + + sk = secp256k1.PrivateKey(self.raw_secret) + self.public_key = PublicKey(sk.pubkey.serialize()[1:]) + + def bech32(self) -> str: + converted_bits = bech32.convertbits(self.raw_secret, 8, 5) + return bech32.bech32_encode("nsec", converted_bits, bech32.Encoding.BECH32) + + def hex(self) -> str: + return self.raw_secret.hex() + + def tweak_add(self, scalar: bytes) -> bytes: + sk = secp256k1.PrivateKey(self.raw_secret) + return sk.tweak_add(scalar) + + def compute_shared_secret(self, public_key_hex: str) -> bytes: + pk = secp256k1.PublicKey(bytes.fromhex("02" + public_key_hex), True) + return pk.ecdh(self.raw_secret, hashfn=copy_x) + + def encrypt_message(self, message: str, public_key_hex: str) -> str: + padder = padding.PKCS7(128).padder() + padded_data = padder.update(message.encode()) + padder.finalize() + + iv = secrets.token_bytes(16) + cipher = Cipher(algorithms.AES(self.compute_shared_secret(public_key_hex)), modes.CBC(iv)) + + encryptor = cipher.encryptor() + encrypted_message = encryptor.update(padded_data) + encryptor.finalize() + + return f"{base64.b64encode(encrypted_message).decode()}?iv={base64.b64encode(iv).decode()}" + + def decrypt_message(self, encoded_message: str, public_key_hex: str) -> str: + encoded_data = encoded_message.split('?iv=') + encoded_content, encoded_iv = encoded_data[0], encoded_data[1] + + iv = base64.b64decode(encoded_iv) + cipher = Cipher(algorithms.AES(self.compute_shared_secret(public_key_hex)), modes.CBC(iv)) + encrypted_content = base64.b64decode(encoded_content) + + decryptor = cipher.decryptor() + decrypted_message = decryptor.update(encrypted_content) + decryptor.finalize() + + unpadder = padding.PKCS7(128).unpadder() + unpadded_data = unpadder.update(decrypted_message) + unpadder.finalize() + + return unpadded_data.decode() + + def sign_message_hash(self, hash: bytes) -> str: + sk = secp256k1.PrivateKey(self.raw_secret) + sig = sk.schnorr_sign(hash, None, raw=True) + return sig.hex() + +ffi = FFI() +@ffi.callback("int (unsigned char *, const unsigned char *, const unsigned char *, void *)") +def copy_x(output, x32, y32, data): + ffi.memmove(output, x32, 32) + return 1 \ No newline at end of file diff --git a/nostr/nostr/message_pool.py b/nostr/nostr/message_pool.py new file mode 100644 index 0000000..472e31e --- /dev/null +++ b/nostr/nostr/message_pool.py @@ -0,0 +1,78 @@ +import json +from queue import Queue +from threading import Lock +from .message_type import RelayMessageType +from .event import Event + + +class EventMessage: + def __init__(self, event: Event, subscription_id: str, url: str) -> None: + self.event = event + self.subscription_id = subscription_id + self.url = url + + +class NoticeMessage: + def __init__(self, content: str, url: str) -> None: + self.content = content + self.url = url + + +class EndOfStoredEventsMessage: + def __init__(self, subscription_id: str, url: str) -> None: + self.subscription_id = subscription_id + self.url = url + + +class MessagePool: + def __init__(self) -> None: + self.events: Queue[EventMessage] = Queue() + self.notices: Queue[NoticeMessage] = Queue() + self.eose_notices: Queue[EndOfStoredEventsMessage] = Queue() + self._unique_events: set = set() + self.lock: Lock = Lock() + + def add_message(self, message: str, url: str): + self._process_message(message, url) + + def get_event(self): + return self.events.get() + + def get_notice(self): + return self.notices.get() + + def get_eose_notice(self): + return self.eose_notices.get() + + def has_events(self): + return self.events.qsize() > 0 + + def has_notices(self): + return self.notices.qsize() > 0 + + def has_eose_notices(self): + return self.eose_notices.qsize() > 0 + + def _process_message(self, message: str, url: str): + message_json = json.loads(message) + message_type = message_json[0] + if message_type == RelayMessageType.EVENT: + subscription_id = message_json[1] + e = message_json[2] + event = Event( + e["pubkey"], + e["content"], + e["created_at"], + e["kind"], + e["tags"], + e["id"], + e["sig"], + ) + with self.lock: + if not event.id in self._unique_events: + self.events.put(EventMessage(event, subscription_id, url)) + self._unique_events.add(event.id) + elif message_type == RelayMessageType.NOTICE: + self.notices.put(NoticeMessage(message_json[1], url)) + elif message_type == RelayMessageType.END_OF_STORED_EVENTS: + self.eose_notices.put(EndOfStoredEventsMessage(message_json[1], url)) diff --git a/nostr/nostr/message_type.py b/nostr/nostr/message_type.py new file mode 100644 index 0000000..3f5206b --- /dev/null +++ b/nostr/nostr/message_type.py @@ -0,0 +1,15 @@ +class ClientMessageType: + EVENT = "EVENT" + REQUEST = "REQ" + CLOSE = "CLOSE" + +class RelayMessageType: + EVENT = "EVENT" + NOTICE = "NOTICE" + END_OF_STORED_EVENTS = "EOSE" + + @staticmethod + def is_valid(type: str) -> bool: + if type == RelayMessageType.EVENT or type == RelayMessageType.NOTICE or type == RelayMessageType.END_OF_STORED_EVENTS: + return True + return False \ No newline at end of file diff --git a/nostr/nostr/relay.py b/nostr/nostr/relay.py new file mode 100644 index 0000000..ad01ff6 --- /dev/null +++ b/nostr/nostr/relay.py @@ -0,0 +1,123 @@ +import json +from threading import Lock +from websocket import WebSocketApp +from .event import Event +from .filter import Filters +from .message_pool import MessagePool +from .message_type import RelayMessageType +from .subscription import Subscription + + +class RelayPolicy: + def __init__(self, should_read: bool = True, should_write: bool = True) -> None: + self.should_read = should_read + self.should_write = should_write + + def to_json_object(self) -> dict[str, bool]: + return {"read": self.should_read, "write": self.should_write} + + +class Relay: + def __init__( + self, + url: str, + policy: RelayPolicy, + message_pool: MessagePool, + subscriptions: dict[str, Subscription] = {}, + ) -> None: + self.url = url + self.policy = policy + self.message_pool = message_pool + self.subscriptions = subscriptions + self.lock = Lock() + self.ws = WebSocketApp( + url, + on_open=self._on_open, + on_message=self._on_message, + on_error=self._on_error, + on_close=self._on_close, + ) + + def connect(self, ssl_options: dict = None): + self.ws.run_forever(sslopt=ssl_options) + + def close(self): + self.ws.close() + + def publish(self, message: str): + self.ws.send(message) + + def add_subscription(self, id, filters: Filters): + with self.lock: + self.subscriptions[id] = Subscription(id, filters) + + def close_subscription(self, id: str) -> None: + with self.lock: + self.subscriptions.pop(id) + + def update_subscription(self, id: str, filters: Filters) -> None: + with self.lock: + subscription = self.subscriptions[id] + subscription.filters = filters + + def to_json_object(self) -> dict: + return { + "url": self.url, + "policy": self.policy.to_json_object(), + "subscriptions": [ + subscription.to_json_object() + for subscription in self.subscriptions.values() + ], + } + + def _on_open(self, class_obj): + pass + + def _on_close(self, class_obj, status_code, message): + pass + + def _on_message(self, class_obj, message: str): + if self._is_valid_message(message): + self.message_pool.add_message(message, self.url) + + def _on_error(self, class_obj, error): + pass + + def _is_valid_message(self, message: str) -> bool: + message = message.strip("\n") + if not message or message[0] != "[" or message[-1] != "]": + return False + + message_json = json.loads(message) + message_type = message_json[0] + if not RelayMessageType.is_valid(message_type): + return False + if message_type == RelayMessageType.EVENT: + if not len(message_json) == 3: + return False + + subscription_id = message_json[1] + with self.lock: + if subscription_id not in self.subscriptions: + return False + + e = message_json[2] + event = Event( + e["pubkey"], + e["content"], + e["created_at"], + e["kind"], + e["tags"], + e["id"], + e["sig"], + ) + if not event.verify(): + return False + + with self.lock: + subscription = self.subscriptions[subscription_id] + + if not subscription.filters.match(event): + return False + + return True diff --git a/nostr/nostr/relay_manager.py b/nostr/nostr/relay_manager.py new file mode 100644 index 0000000..e4d177e --- /dev/null +++ b/nostr/nostr/relay_manager.py @@ -0,0 +1,43 @@ +import threading +from .filter import Filters +from .message_pool import MessagePool +from .relay import Relay, RelayPolicy + +class RelayManager: + def __init__(self) -> None: + self.relays: dict[str, Relay] = {} + self.message_pool = MessagePool() + + def add_relay(self, url: str, read: bool=True, write: bool=True, subscriptions={}): + policy = RelayPolicy(read, write) + relay = Relay(url, policy, self.message_pool, subscriptions) + self.relays[url] = relay + + def remove_relay(self, url: str): + self.relays.pop(url) + + def add_subscription(self, id: str, filters: Filters): + for relay in self.relays.values(): + relay.add_subscription(id, filters) + + def close_subscription(self, id: str): + for relay in self.relays.values(): + relay.close_subscription(id) + + def open_connections(self, ssl_options: dict=None): + for relay in self.relays.values(): + threading.Thread( + target=relay.connect, + args=(ssl_options,), + name=f"{relay.url}-thread" + ).start() + + def close_connections(self): + for relay in self.relays.values(): + relay.close() + + def publish_message(self, message: str): + for relay in self.relays.values(): + if relay.policy.should_write: + relay.publish(message) + diff --git a/nostr/nostr/subscription.py b/nostr/nostr/subscription.py new file mode 100644 index 0000000..7afba20 --- /dev/null +++ b/nostr/nostr/subscription.py @@ -0,0 +1,12 @@ +from .filter import Filters + +class Subscription: + def __init__(self, id: str, filters: Filters=None) -> None: + self.id = id + self.filters = filters + + def to_json_object(self): + return { + "id": self.id, + "filters": self.filters.to_json_array() + } diff --git a/nostr/relay.py b/nostr/relay.py new file mode 100644 index 0000000..16667eb --- /dev/null +++ b/nostr/relay.py @@ -0,0 +1,124 @@ +import json +from threading import Lock +from websocket import WebSocketApp +from .event import Event +from .filter import Filters +from .message_pool import MessagePool +from .message_type import RelayMessageType +from .subscription import Subscription + + +class RelayPolicy: + def __init__(self, should_read: bool = True, should_write: bool = True) -> None: + self.should_read = should_read + self.should_write = should_write + + def to_json_object(self) -> dict[str, bool]: + return {"read": self.should_read, "write": self.should_write} + + +class Relay: + def __init__( + self, + url: str, + policy: RelayPolicy, + message_pool: MessagePool, + subscriptions: dict[str, Subscription] = {}, + ) -> None: + self.url = url + self.policy = policy + self.message_pool = message_pool + self.subscriptions = subscriptions + self.lock = Lock() + self.ws = WebSocketApp( + url, + on_open=self._on_open, + on_message=self._on_message, + on_error=self._on_error, + on_close=self._on_close, + ) + + def connect(self, ssl_options: dict = None): + self.ws.run_forever(sslopt=ssl_options) + + def close(self): + self.ws.close() + + def publish(self, message: str): + self.ws.send(message) + + def add_subscription(self, id, filters: Filters): + with self.lock: + self.subscriptions[id] = Subscription(id, filters) + + def close_subscription(self, id: str) -> None: + with self.lock: + self.subscriptions.pop(id) + + def update_subscription(self, id: str, filters: Filters) -> None: + with self.lock: + subscription = self.subscriptions[id] + subscription.filters = filters + + def to_json_object(self) -> dict: + return { + "url": self.url, + "policy": self.policy.to_json_object(), + "subscriptions": [ + subscription.to_json_object() + for subscription in self.subscriptions.values() + ], + } + + def _on_open(self, class_obj): + pass + + def _on_close(self, class_obj, status_code, message): + pass + + def _on_message(self, class_obj, message: str): + if self._is_valid_message(message): + self.message_pool.add_message(message, self.url) + + def _on_error(self, class_obj, error): + # print(error) + pass + + def _is_valid_message(self, message: str) -> bool: + message = message.strip("\n") + if not message or message[0] != "[" or message[-1] != "]": + return False + + message_json = json.loads(message) + message_type = message_json[0] + if not RelayMessageType.is_valid(message_type): + return False + if message_type == RelayMessageType.EVENT: + if not len(message_json) == 3: + return False + + subscription_id = message_json[1] + with self.lock: + if subscription_id not in self.subscriptions: + return False + + e = message_json[2] + event = Event( + e["pubkey"], + e["content"], + e["created_at"], + e["kind"], + e["tags"], + e["id"], + e["sig"], + ) + if not event.verify(): + return False + + with self.lock: + subscription = self.subscriptions[subscription_id] + + if not subscription.filters.match(event): + return False + + return True diff --git a/nostr/relay_manager.py b/nostr/relay_manager.py new file mode 100644 index 0000000..e4d177e --- /dev/null +++ b/nostr/relay_manager.py @@ -0,0 +1,43 @@ +import threading +from .filter import Filters +from .message_pool import MessagePool +from .relay import Relay, RelayPolicy + +class RelayManager: + def __init__(self) -> None: + self.relays: dict[str, Relay] = {} + self.message_pool = MessagePool() + + def add_relay(self, url: str, read: bool=True, write: bool=True, subscriptions={}): + policy = RelayPolicy(read, write) + relay = Relay(url, policy, self.message_pool, subscriptions) + self.relays[url] = relay + + def remove_relay(self, url: str): + self.relays.pop(url) + + def add_subscription(self, id: str, filters: Filters): + for relay in self.relays.values(): + relay.add_subscription(id, filters) + + def close_subscription(self, id: str): + for relay in self.relays.values(): + relay.close_subscription(id) + + def open_connections(self, ssl_options: dict=None): + for relay in self.relays.values(): + threading.Thread( + target=relay.connect, + args=(ssl_options,), + name=f"{relay.url}-thread" + ).start() + + def close_connections(self): + for relay in self.relays.values(): + relay.close() + + def publish_message(self, message: str): + for relay in self.relays.values(): + if relay.policy.should_write: + relay.publish(message) + diff --git a/nostr/subscription.py b/nostr/subscription.py new file mode 100644 index 0000000..7afba20 --- /dev/null +++ b/nostr/subscription.py @@ -0,0 +1,12 @@ +from .filter import Filters + +class Subscription: + def __init__(self, id: str, filters: Filters=None) -> None: + self.id = id + self.filters = filters + + def to_json_object(self): + return { + "id": self.id, + "filters": self.filters.to_json_array() + } diff --git a/poetry.lock b/poetry.lock index c95e088..c3cbd9f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -133,6 +133,25 @@ tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.1 [package.extras] toml = ["tomli"] +[[package]] +name = "cryptography" +version = "38.0.4" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +cffi = ">=1.12" + +[package.extras] +docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"] +docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"] +pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] +sdist = ["setuptools-rust (>=0.11.4)"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["hypothesis (>=1.11.4,!=3.79.2)", "iso8601", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pytz"] + [[package]] name = "ecdsa" version = "0.18.0" @@ -371,6 +390,14 @@ category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +[[package]] +name = "pycryptodomex" +version = "3.16.0" +description = "Cryptographic library for Python" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + [[package]] name = "pydantic" version = "1.10.2" @@ -640,6 +667,19 @@ typing-extensions = {version = "*", markers = "python_version < \"3.8\""} [package.extras] standard = ["colorama (>=0.4)", "httptools (>=0.4.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.0)"] +[[package]] +name = "websocket-client" +version = "1.3.3" +description = "WebSocket client for Python with low level API options" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.extras] +docs = ["Sphinx (>=3.4)", "sphinx-rtd-theme (>=0.5)"] +optional = ["python-socks", "wsaccel"] +test = ["websockets"] + [[package]] name = "win32-setctime" version = "1.1.0" @@ -666,7 +706,7 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools" [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "aa0c3cf3a023b4143939128be203cf0c519341abc7cd7ef0b200694f8b925b78" +content-hash = "d26c1683860705c1936769b5baade31986d00b3318092971ebaed265a138fb96" [metadata.files] anyio = [ @@ -843,6 +883,34 @@ coverage = [ {file = "coverage-6.5.0-pp36.pp37.pp38-none-any.whl", hash = "sha256:1431986dac3923c5945271f169f59c45b8802a114c8f548d611f2015133df77a"}, {file = "coverage-6.5.0.tar.gz", hash = "sha256:f642e90754ee3e06b0e7e51bce3379590e76b7f76b708e1a71ff043f87025c84"}, ] +cryptography = [ + {file = "cryptography-38.0.4-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:2fa36a7b2cc0998a3a4d5af26ccb6273f3df133d61da2ba13b3286261e7efb70"}, + {file = "cryptography-38.0.4-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:1f13ddda26a04c06eb57119caf27a524ccae20533729f4b1e4a69b54e07035eb"}, + {file = "cryptography-38.0.4-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:2ec2a8714dd005949d4019195d72abed84198d877112abb5a27740e217e0ea8d"}, + {file = "cryptography-38.0.4-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50a1494ed0c3f5b4d07650a68cd6ca62efe8b596ce743a5c94403e6f11bf06c1"}, + {file = "cryptography-38.0.4-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a10498349d4c8eab7357a8f9aa3463791292845b79597ad1b98a543686fb1ec8"}, + {file = "cryptography-38.0.4-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:10652dd7282de17990b88679cb82f832752c4e8237f0c714be518044269415db"}, + {file = "cryptography-38.0.4-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:bfe6472507986613dc6cc00b3d492b2f7564b02b3b3682d25ca7f40fa3fd321b"}, + {file = "cryptography-38.0.4-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:ce127dd0a6a0811c251a6cddd014d292728484e530d80e872ad9806cfb1c5b3c"}, + {file = "cryptography-38.0.4-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:53049f3379ef05182864d13bb9686657659407148f901f3f1eee57a733fb4b00"}, + {file = "cryptography-38.0.4-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:8a4b2bdb68a447fadebfd7d24855758fe2d6fecc7fed0b78d190b1af39a8e3b0"}, + {file = "cryptography-38.0.4-cp36-abi3-win32.whl", hash = "sha256:1d7e632804a248103b60b16fb145e8df0bc60eed790ece0d12efe8cd3f3e7744"}, + {file = "cryptography-38.0.4-cp36-abi3-win_amd64.whl", hash = "sha256:8e45653fb97eb2f20b8c96f9cd2b3a0654d742b47d638cf2897afbd97f80fa6d"}, + {file = "cryptography-38.0.4-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca57eb3ddaccd1112c18fc80abe41db443cc2e9dcb1917078e02dfa010a4f353"}, + {file = "cryptography-38.0.4-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:c9e0d79ee4c56d841bd4ac6e7697c8ff3c8d6da67379057f29e66acffcd1e9a7"}, + {file = "cryptography-38.0.4-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:0e70da4bdff7601b0ef48e6348339e490ebfb0cbe638e083c9c41fb49f00c8bd"}, + {file = "cryptography-38.0.4-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:998cd19189d8a747b226d24c0207fdaa1e6658a1d3f2494541cb9dfbf7dcb6d2"}, + {file = "cryptography-38.0.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67461b5ebca2e4c2ab991733f8ab637a7265bb582f07c7c88914b5afb88cb95b"}, + {file = "cryptography-38.0.4-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:4eb85075437f0b1fd8cd66c688469a0c4119e0ba855e3fef86691971b887caf6"}, + {file = "cryptography-38.0.4-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3178d46f363d4549b9a76264f41c6948752183b3f587666aff0555ac50fd7876"}, + {file = "cryptography-38.0.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:6391e59ebe7c62d9902c24a4d8bcbc79a68e7c4ab65863536127c8a9cd94043b"}, + {file = "cryptography-38.0.4-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:78e47e28ddc4ace41dd38c42e6feecfdadf9c3be2af389abbfeef1ff06822285"}, + {file = "cryptography-38.0.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fb481682873035600b5502f0015b664abc26466153fab5c6bc92c1ea69d478b"}, + {file = "cryptography-38.0.4-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:4367da5705922cf7070462e964f66e4ac24162e22ab0a2e9d31f1b270dd78083"}, + {file = "cryptography-38.0.4-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b4cad0cea995af760f82820ab4ca54e5471fc782f70a007f31531957f43e9dee"}, + {file = "cryptography-38.0.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:80ca53981ceeb3241998443c4964a387771588c4e4a5d92735a493af868294f9"}, + {file = "cryptography-38.0.4.tar.gz", hash = "sha256:175c1a818b87c9ac80bb7377f5520b7f31b3ef2a0004e2420319beadedb67290"}, +] ecdsa = [ {file = "ecdsa-0.18.0-py2.py3-none-any.whl", hash = "sha256:80600258e7ed2f16b9aa1d7c295bd70194109ad5a30fdee0eaeefef1d4c559dd"}, {file = "ecdsa-0.18.0.tar.gz", hash = "sha256:190348041559e21b22a1d65cee485282ca11a6f81d503fddb84d5017e9ed1e49"}, @@ -940,6 +1008,34 @@ pycparser = [ {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, ] +pycryptodomex = [ + {file = "pycryptodomex-3.16.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:b3d04c00d777c36972b539fb79958790126847d84ec0129fce1efef250bfe3ce"}, + {file = "pycryptodomex-3.16.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:e5a670919076b71522c7d567a9043f66f14b202414a63c3a078b5831ae342c03"}, + {file = "pycryptodomex-3.16.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:ce338a9703f54b2305a408fc9890eb966b727ce72b69f225898bb4e9d9ed3f1f"}, + {file = "pycryptodomex-3.16.0-cp27-cp27m-manylinux2014_aarch64.whl", hash = "sha256:a1c0ae7123448ecb034c75c713189cb00ebe2d415b11682865b6c54d200d9c93"}, + {file = "pycryptodomex-3.16.0-cp27-cp27m-win32.whl", hash = "sha256:8851585ff19871e5d69e1790f4ca5f6fd1699d6b8b14413b472a4c0dbc7ea780"}, + {file = "pycryptodomex-3.16.0-cp27-cp27m-win_amd64.whl", hash = "sha256:8dd2d9e3c617d0712ed781a77efd84ea579e76c5f9b2a4bc0b684ebeddf868b2"}, + {file = "pycryptodomex-3.16.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:2ad9bb86b355b6104796567dd44c215b3dc953ef2fae5e0bdfb8516731df92cf"}, + {file = "pycryptodomex-3.16.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:e25a2f5667d91795f9417cb856f6df724ccdb0cdd5cbadb212ee9bf43946e9f8"}, + {file = "pycryptodomex-3.16.0-cp27-cp27mu-manylinux2014_aarch64.whl", hash = "sha256:b0789a8490114a2936ed77c87792cfe77582c829cb43a6d86ede0f9624ba8aa3"}, + {file = "pycryptodomex-3.16.0-cp35-abi3-macosx_10_9_x86_64.whl", hash = "sha256:0da835af786fdd1c9930994c78b23e88d816dc3f99aa977284a21bbc26d19735"}, + {file = "pycryptodomex-3.16.0-cp35-abi3-manylinux2014_aarch64.whl", hash = "sha256:22aed0868622d95179217c298e37ed7410025c7b29dac236d3230617d1e4ed56"}, + {file = "pycryptodomex-3.16.0-cp35-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1619087fb5b31510b0b0b058a54f001a5ffd91e6ffee220d9913064519c6a69d"}, + {file = "pycryptodomex-3.16.0-cp35-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:70288d9bfe16b2fd0d20b6c365db614428f1bcde7b20d56e74cf88ade905d9eb"}, + {file = "pycryptodomex-3.16.0-cp35-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:7993d26dae4d83b8f4ce605bb0aecb8bee330bb3c95475ef06f3694403621e71"}, + {file = "pycryptodomex-3.16.0-cp35-abi3-musllinux_1_1_i686.whl", hash = "sha256:1cda60207be8c1cf0b84b9138f9e3ca29335013d2b690774a5e94678ff29659a"}, + {file = "pycryptodomex-3.16.0-cp35-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:04610536921c1ec7adba158ef570348550c9f3a40bc24be9f8da2ef7ab387981"}, + {file = "pycryptodomex-3.16.0-cp35-abi3-win32.whl", hash = "sha256:daa67f5ebb6fbf1ee9c90decaa06ca7fc88a548864e5e484d52b0920a57fe8a5"}, + {file = "pycryptodomex-3.16.0-cp35-abi3-win_amd64.whl", hash = "sha256:231dc8008cbdd1ae0e34645d4523da2dbc7a88c325f0d4a59635a86ee25b41dd"}, + {file = "pycryptodomex-3.16.0-pp27-pypy_73-macosx_10_9_x86_64.whl", hash = "sha256:4dbbe18cc232b5980c7633972ae5417d0df76fe89e7db246eefd17ef4d8e6d7a"}, + {file = "pycryptodomex-3.16.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:893f8a97d533c66cc3a56e60dd3ed40a3494ddb4aafa7e026429a08772f8a849"}, + {file = "pycryptodomex-3.16.0-pp27-pypy_73-win32.whl", hash = "sha256:6a465e4f856d2a4f2a311807030c89166529ccf7ccc65bef398de045d49144b6"}, + {file = "pycryptodomex-3.16.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ba57ac7861fd2c837cdb33daf822f2a052ff57dd769a2107807f52a36d0e8d38"}, + {file = "pycryptodomex-3.16.0-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f2b971a7b877348a27dcfd0e772a0343fb818df00b74078e91c008632284137d"}, + {file = "pycryptodomex-3.16.0-pp37-pypy37_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e2453162f473c1eae4826eb10cd7bce19b5facac86d17fb5f29a570fde145abd"}, + {file = "pycryptodomex-3.16.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:0ba28aa97cdd3ff5ed1a4f2b7f5cd04e721166bd75bd2b929e2734433882b583"}, + {file = "pycryptodomex-3.16.0.tar.gz", hash = "sha256:e9ba9d8ed638733c9e95664470b71d624a6def149e2db6cc52c1aca5a6a2df1d"}, +] pydantic = [ {file = "pydantic-1.10.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bb6ad4489af1bac6955d38ebcb95079a836af31e4c4f74aba1ca05bb9f6027bd"}, {file = "pydantic-1.10.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a1f5a63a6dfe19d719b1b6e6106561869d2efaca6167f84f5ab9347887d78b98"}, @@ -1134,6 +1230,10 @@ uvicorn = [ {file = "uvicorn-0.18.3-py3-none-any.whl", hash = "sha256:0abd429ebb41e604ed8d2be6c60530de3408f250e8d2d84967d85ba9e86fe3af"}, {file = "uvicorn-0.18.3.tar.gz", hash = "sha256:9a66e7c42a2a95222f76ec24a4b754c158261c4696e683b9dadc72b590e0311b"}, ] +websocket-client = [ + {file = "websocket-client-1.3.3.tar.gz", hash = "sha256:d58c5f284d6a9bf8379dab423259fe8f85b70d5fa5d2916d5791a84594b122b1"}, + {file = "websocket_client-1.3.3-py3-none-any.whl", hash = "sha256:5d55652dc1d0b3c734f044337d929aaf83f4f9138816ec680c1aefefb4dc4877"}, +] win32-setctime = [ {file = "win32_setctime-1.1.0-py3-none-any.whl", hash = "sha256:231db239e959c2fe7eb1d7dc129f11172354f98361c4fa2d6d2d7e278baa8aad"}, {file = "win32_setctime-1.1.0.tar.gz", hash = "sha256:15cf5750465118d6929ae4de4eb46e8edae9a5634350c01ba582df868e932cb2"}, diff --git a/pyproject.toml b/pyproject.toml index 835dcfb..75fe525 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,6 +24,9 @@ sqlalchemy-aio = "^0.17.0" python-bitcoinlib = "^0.11.2" h11 = "0.12.0" PySocks = "^1.7.1" +cryptography = "^38.0.4" +websocket-client = "1.3.3" +pycryptodomex = "^3.16.0" [tool.poetry.dev-dependencies] black = {version = "^22.8.0", allow-prereleases = true} From 6dc9937b9db1f2802214ae3ae2d66f72486faf8c Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Fri, 23 Dec 2022 20:02:48 +0100 Subject: [PATCH 02/15] updates --- nostr/nostr/client/client.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/nostr/nostr/client/client.py b/nostr/nostr/client/client.py index 341778e..68ec69d 100644 --- a/nostr/nostr/client/client.py +++ b/nostr/nostr/client/client.py @@ -3,7 +3,7 @@ import ssl import time import json import os -import base64 +import base64 from nostr.event import Event from nostr.relay_manager import RelayManager @@ -21,8 +21,9 @@ from . import cbc class NostrClient: relays = [ - "wss://nostr.zebedee.cloud" - ] # ["wss://nostr.oxtr.dev"] # ["wss://relay.nostr.info"] "wss://nostr-pub.wellorder.net" "ws://91.237.88.218:2700", "wss://nostrrr.bublina.eu.org", ""wss://nostr-relay.freeberty.net"", , "wss://nostr.oxtr.dev", "wss://relay.nostr.info", "wss://nostr-pub.wellorder.net" , "wss://relayer.fiatjaf.com", "wss://nodestr.fmt.wiz.biz/", "wss://no.str.cr" + "wss://nostr.zebedee.cloud", + "wss://nostr-relay.digitalmob.ro", + ] # ["wss://nostr.oxtr.dev"] # ["wss://relay.nostr.info"] "wss://nostr-pub.wellorder.net" "ws://91.237.88.218:2700", "wss://nostrrr.bublina.eu.org", ""wss://nostr-relay.freeberty.net"", , "wss://nostr.oxtr.dev", "wss://relay.nostr.info", "wss://nostr-pub.wellorder.net" , "wss://relayer.fiatjaf.com", "wss://nodestr.fmt.wiz.biz/", "wss://no.str.cr", "wss://nostr-relay.digitalmob.ro" relay_manager = RelayManager() private_key: PrivateKey public_key: PublicKey @@ -83,9 +84,7 @@ class NostrClient: def dm(self, message: str, to_pubkey: PublicKey): - shared_secret = self.private_key.compute_shared_secret( - to_pubkey.hex() - ) + shared_secret = self.private_key.compute_shared_secret(to_pubkey.hex()) # print("shared secret: ", shared_secret.hex()) # print("plain text:", message) @@ -94,7 +93,6 @@ class NostrClient: # print("encrypt iv: ", iv) content = f"{base64.b64encode(enc_text).decode('utf-8')}?iv={base64.b64encode(iv).decode('utf-8')}" - event = Event( self.public_key.hex(), content, @@ -131,7 +129,7 @@ class NostrClient: while True: while self.relay_manager.message_pool.has_events(): event_msg = self.relay_manager.message_pool.get_event() - + if "?iv=" in event_msg.event.content: try: shared_secret = self.private_key.compute_shared_secret( @@ -151,7 +149,7 @@ class NostrClient: except: pass # else: - # print(f"\nFrom {event_msg.event.public_key[:5]}...: {event_msg.event.content}") + # print(f"\nFrom {event_msg.event.public_key[:5]}...: {event_msg.event.content}") break time.sleep(0.1) @@ -162,4 +160,3 @@ class NostrClient: print(event_msg.event.content) break time.sleep(0.1) - From 01c6e12b5a0539b534f4ecd8075e586dc1465737 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Mon, 26 Dec 2022 16:12:31 +0100 Subject: [PATCH 03/15] add nostr --- .env.example | 7 +- .gitmodules | 3 + cashu/core/settings.py | 2 + cashu/nostr | 1 + cashu/wallet/cli.py | 43 ++--- nostr/__init__.py | 0 nostr/bech32.py | 137 --------------- nostr/client/__init__.py | 0 nostr/client/aes.py | 86 ---------- nostr/client/cbc.py | 41 ----- nostr/client/client.py | 165 ------------------ nostr/event.py | 65 -------- nostr/filter.py | 81 --------- nostr/key.py | 86 ---------- nostr/message_pool.py | 78 --------- nostr/message_type.py | 15 -- nostr/nostr/__init__.py | 0 nostr/nostr/bech32.py | 137 --------------- nostr/nostr/client/__init__.py | 0 nostr/nostr/client/aes.py | 86 ---------- nostr/nostr/client/cbc.py | 41 ----- nostr/nostr/client/client.py | 162 ------------------ nostr/nostr/event.py | 65 -------- nostr/nostr/filter.py | 81 --------- nostr/nostr/key.py | 86 ---------- nostr/nostr/message_pool.py | 78 --------- nostr/nostr/message_type.py | 15 -- nostr/nostr/relay.py | 123 -------------- nostr/nostr/relay_manager.py | 43 ----- nostr/nostr/subscription.py | 12 -- nostr/relay.py | 124 -------------- nostr/relay_manager.py | 43 ----- nostr/subscription.py | 12 -- poetry.lock | 297 +++++++++++++++------------------ 34 files changed, 174 insertions(+), 2041 deletions(-) create mode 100644 .gitmodules create mode 160000 cashu/nostr delete mode 100644 nostr/__init__.py delete mode 100644 nostr/bech32.py delete mode 100644 nostr/client/__init__.py delete mode 100644 nostr/client/aes.py delete mode 100644 nostr/client/cbc.py delete mode 100644 nostr/client/client.py delete mode 100644 nostr/event.py delete mode 100644 nostr/filter.py delete mode 100644 nostr/key.py delete mode 100644 nostr/message_pool.py delete mode 100644 nostr/message_type.py delete mode 100644 nostr/nostr/__init__.py delete mode 100644 nostr/nostr/bech32.py delete mode 100644 nostr/nostr/client/__init__.py delete mode 100644 nostr/nostr/client/aes.py delete mode 100644 nostr/nostr/client/cbc.py delete mode 100644 nostr/nostr/client/client.py delete mode 100644 nostr/nostr/event.py delete mode 100644 nostr/nostr/filter.py delete mode 100644 nostr/nostr/key.py delete mode 100644 nostr/nostr/message_pool.py delete mode 100644 nostr/nostr/message_type.py delete mode 100644 nostr/nostr/relay.py delete mode 100644 nostr/nostr/relay_manager.py delete mode 100644 nostr/nostr/subscription.py delete mode 100644 nostr/relay.py delete mode 100644 nostr/relay_manager.py delete mode 100644 nostr/subscription.py diff --git a/.env.example b/.env.example index 5ae391c..0de8074 100644 --- a/.env.example +++ b/.env.example @@ -29,4 +29,9 @@ LIGHTNING_FEE_PERCENT=1.0 LIGHTNING_RESERVE_FEE_MIN=4000 LNBITS_ENDPOINT=https://legend.lnbits.com -LNBITS_KEY=yourkeyasdasdasd \ No newline at end of file +LNBITS_KEY=yourkeyasdasdasd + +# NOSTR + +# this is the hex private key on which you want to receive nostr DMs to +# NOSTR_PRIVATE_KEY=hex_nostrprivatekey_here \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..eaa87cc --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "cashu/nostr"] + path = cashu/nostr + url = https://github.com/callebtc/python-nostr/ diff --git a/cashu/core/settings.py b/cashu/core/settings.py index effd384..b35077a 100644 --- a/cashu/core/settings.py +++ b/cashu/core/settings.py @@ -52,5 +52,7 @@ if not MINT_URL: LNBITS_ENDPOINT = env.str("LNBITS_ENDPOINT", default=None) LNBITS_KEY = env.str("LNBITS_KEY", default=None) +NOSTR_PRIVATE_KEY = env.str("NOSTR_PRIVATE_KEY", default=None) + MAX_ORDER = 64 VERSION = "0.6.0" diff --git a/cashu/nostr b/cashu/nostr new file mode 160000 index 0000000..dfca8bf --- /dev/null +++ b/cashu/nostr @@ -0,0 +1 @@ +Subproject commit dfca8bfdc59732fca4c04d3ce762dedb00b2598f diff --git a/cashu/wallet/cli.py b/cashu/wallet/cli.py index d454181..06a0863 100644 --- a/cashu/wallet/cli.py +++ b/cashu/wallet/cli.py @@ -15,6 +15,7 @@ from os.path import isdir, join import click from loguru import logger +import threading from cashu.core.base import Proof from cashu.core.helpers import sum_proofs @@ -28,6 +29,7 @@ from cashu.core.settings import ( SOCKS_HOST, SOCKS_PORT, TOR, + NOSTR_PRIVATE_KEY, VERSION, ) from cashu.tor.tor import TorProxy @@ -39,8 +41,9 @@ from cashu.wallet.crud import ( ) from cashu.wallet.wallet import Wallet as Wallet -from nostr.client.client import NostrClient -from nostr.key import PublicKey +from cashu.nostr.nostr.client.client import NostrClient +from cashu.nostr.nostr.key import PublicKey +from cashu.nostr.nostr.event import Event async def init_wallet(wallet: Wallet): @@ -438,29 +441,36 @@ async def info(ctx): return -@cli.command("nostr", help="Receive tokens via nostr.") +@cli.command("nreceive", help="Receive tokens via nostr.") @click.pass_context @coro async def nostr(ctx): wallet: Wallet = ctx.obj["WALLET"] await wallet.load_mint() - client = NostrClient( - privatekey_hex="bfc6e7b0b998645d45aa451a3b9a3174bfe696fba78e86a86637a16f43e6c683" - ) - print(f"Your nostr public key: {client.public_key.hex()}") + if NOSTR_PRIVATE_KEY is None: + print( + "Warning!\n\nYou don't have a private key set in your .env file. I will create a random private key for this session but I will not remember it. If you lose this key, you will lose access to the DMs you receive on it." + ) + print("") + client = NostrClient(privatekey_hex=NOSTR_PRIVATE_KEY) + await asyncio.sleep(2) - def get_token_callback(token): + def get_token_callback(event: Event, decrypted_content): + print( + f"From {event.public_key[:3]}..{event.public_key[-3:]}: {decrypted_content}" + ) try: - proofs = [Proof(**p) for p in json.loads(base64.urlsafe_b64decode(token))] + proofs = [ + Proof(**p) + for p in json.loads(base64.urlsafe_b64decode(decrypted_content)) + ] wallet: Wallet = ctx.obj["WALLET"] asyncio.run(wallet.redeem(proofs)) wallet.status() except Exception as e: pass - import threading - t = threading.Thread( target=client.get_dm, args=( @@ -472,7 +482,7 @@ async def nostr(ctx): t.start() -@cli.command("nostrsend", help="Send tokens via nostr.") +@cli.command("nsend", help="Send tokens via nostr.") @click.argument("amount", type=int) @click.argument( "pubkey", @@ -490,16 +500,11 @@ async def nostrsend(ctx, amount: int, pubkey: str): ) token = await wallet.serialize_proofs(send_proofs) - from random import randrange - - # token = f"Token {randrange(1000)}" print(token) wallet.status() - client = NostrClient( - privatekey_hex="bfc6e7b0b598645d45aa451a3b9a3174bfe696fba78e86a86637a16f4ee6d683" - ) + client = NostrClient(privatekey_hex=NOSTR_PRIVATE_KEY) await asyncio.sleep(1) client.dm(token, PublicKey(bytes.fromhex(pubkey))) - print("Sent") + print(f"Token sent to {pubkey}") client.close() diff --git a/nostr/__init__.py b/nostr/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/nostr/bech32.py b/nostr/bech32.py deleted file mode 100644 index b068de7..0000000 --- a/nostr/bech32.py +++ /dev/null @@ -1,137 +0,0 @@ -# Copyright (c) 2017, 2020 Pieter Wuille -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -"""Reference implementation for Bech32/Bech32m and segwit addresses.""" - - -from enum import Enum - -class Encoding(Enum): - """Enumeration type to list the various supported encodings.""" - BECH32 = 1 - BECH32M = 2 - -CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" -BECH32M_CONST = 0x2bc830a3 - -def bech32_polymod(values): - """Internal function that computes the Bech32 checksum.""" - generator = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3] - chk = 1 - for value in values: - top = chk >> 25 - chk = (chk & 0x1ffffff) << 5 ^ value - for i in range(5): - chk ^= generator[i] if ((top >> i) & 1) else 0 - return chk - - -def bech32_hrp_expand(hrp): - """Expand the HRP into values for checksum computation.""" - return [ord(x) >> 5 for x in hrp] + [0] + [ord(x) & 31 for x in hrp] - - -def bech32_verify_checksum(hrp, data): - """Verify a checksum given HRP and converted data characters.""" - const = bech32_polymod(bech32_hrp_expand(hrp) + data) - if const == 1: - return Encoding.BECH32 - if const == BECH32M_CONST: - return Encoding.BECH32M - return None - -def bech32_create_checksum(hrp, data, spec): - """Compute the checksum values given HRP and data.""" - values = bech32_hrp_expand(hrp) + data - const = BECH32M_CONST if spec == Encoding.BECH32M else 1 - polymod = bech32_polymod(values + [0, 0, 0, 0, 0, 0]) ^ const - return [(polymod >> 5 * (5 - i)) & 31 for i in range(6)] - - -def bech32_encode(hrp, data, spec): - """Compute a Bech32 string given HRP and data values.""" - combined = data + bech32_create_checksum(hrp, data, spec) - return hrp + '1' + ''.join([CHARSET[d] for d in combined]) - -def bech32_decode(bech): - """Validate a Bech32/Bech32m string, and determine HRP and data.""" - if ((any(ord(x) < 33 or ord(x) > 126 for x in bech)) or - (bech.lower() != bech and bech.upper() != bech)): - return (None, None, None) - bech = bech.lower() - pos = bech.rfind('1') - if pos < 1 or pos + 7 > len(bech) or len(bech) > 90: - return (None, None, None) - if not all(x in CHARSET for x in bech[pos+1:]): - return (None, None, None) - hrp = bech[:pos] - data = [CHARSET.find(x) for x in bech[pos+1:]] - spec = bech32_verify_checksum(hrp, data) - if spec is None: - return (None, None, None) - return (hrp, data[:-6], spec) - -def convertbits(data, frombits, tobits, pad=True): - """General power-of-2 base conversion.""" - acc = 0 - bits = 0 - ret = [] - maxv = (1 << tobits) - 1 - max_acc = (1 << (frombits + tobits - 1)) - 1 - for value in data: - if value < 0 or (value >> frombits): - return None - acc = ((acc << frombits) | value) & max_acc - bits += frombits - while bits >= tobits: - bits -= tobits - ret.append((acc >> bits) & maxv) - if pad: - if bits: - ret.append((acc << (tobits - bits)) & maxv) - elif bits >= frombits or ((acc << (tobits - bits)) & maxv): - return None - return ret - - -def decode(hrp, addr): - """Decode a segwit address.""" - hrpgot, data, spec = bech32_decode(addr) - if hrpgot != hrp: - return (None, None) - decoded = convertbits(data[1:], 5, 8, False) - if decoded is None or len(decoded) < 2 or len(decoded) > 40: - return (None, None) - if data[0] > 16: - return (None, None) - if data[0] == 0 and len(decoded) != 20 and len(decoded) != 32: - return (None, None) - if data[0] == 0 and spec != Encoding.BECH32 or data[0] != 0 and spec != Encoding.BECH32M: - return (None, None) - return (data[0], decoded) - - -def encode(hrp, witver, witprog): - """Encode a segwit address.""" - spec = Encoding.BECH32 if witver == 0 else Encoding.BECH32M - ret = bech32_encode(hrp, [witver] + convertbits(witprog, 8, 5), spec) - if decode(hrp, ret) == (None, None): - return None - return ret diff --git a/nostr/client/__init__.py b/nostr/client/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/nostr/client/aes.py b/nostr/client/aes.py deleted file mode 100644 index 9d20224..0000000 --- a/nostr/client/aes.py +++ /dev/null @@ -1,86 +0,0 @@ -import base64 -import getpass -from hashlib import md5 - -from Cryptodome import Random -from Cryptodome.Cipher import AES - -BLOCK_SIZE = 16 - -import getpass - - -class AESCipher(object): - """This class is compatible with crypto-js/aes.js - - Encrypt and decrypt in Javascript using: - import AES from "crypto-js/aes.js"; - import Utf8 from "crypto-js/enc-utf8.js"; - AES.encrypt(decrypted, password).toString() - AES.decrypt(encrypted, password).toString(Utf8); - - """ - - def __init__(self, key=None, description=""): - self.key = key - self.description = description + " " - - def pad(self, data): - length = BLOCK_SIZE - (len(data) % BLOCK_SIZE) - return data + (chr(length) * length).encode() - - def unpad(self, data): - return data[: -(data[-1] if type(data[-1]) == int else ord(data[-1]))] - - @property - def passphrase(self): - passphrase = self.key if self.key is not None else None - if passphrase is None: - passphrase = getpass.getpass(f"Enter {self.description}password:") - return passphrase - - def bytes_to_key(self, data, salt, output=48): - # extended from https://gist.github.com/gsakkis/4546068 - assert len(salt) == 8, len(salt) - data += salt - key = md5(data).digest() - final_key = key - while len(final_key) < output: - key = md5(key + data).digest() - final_key += key - return final_key[:output] - - def decrypt(self, encrypted: str) -> str: # type: ignore - """Decrypts a string using AES-256-CBC.""" - passphrase = self.passphrase - encrypted = base64.b64decode(encrypted) # type: ignore - assert encrypted[0:8] == b"Salted__" - salt = encrypted[8:16] - key_iv = self.bytes_to_key(passphrase.encode(), salt, 32 + 16) - key = key_iv[:32] - iv = key_iv[32:] - aes = AES.new(key, AES.MODE_CBC, iv) - try: - return self.unpad(aes.decrypt(encrypted[16:])).decode() # type: ignore - except UnicodeDecodeError: - raise ValueError("Wrong passphrase") - - def encrypt(self, message: bytes) -> str: - passphrase = self.passphrase - salt = Random.new().read(8) - key_iv = self.bytes_to_key(passphrase.encode(), salt, 32 + 16) - key = key_iv[:32] - iv = key_iv[32:] - aes = AES.new(key, AES.MODE_CBC, iv) - return base64.b64encode( - b"Salted__" + salt + aes.encrypt(self.pad(message)) - ).decode() - - -# # if this file is executed directly, ask for a macaroon and encrypt it -# if __name__ == "__main__": -# macaroon = input("Enter macaroon: ") -# macaroon = load_macaroon(macaroon) -# macaroon = AESCipher(description="encryption").encrypt(macaroon.encode()) -# logger.info("Encrypted macaroon:") -# logger.info(macaroon) diff --git a/nostr/client/cbc.py b/nostr/client/cbc.py deleted file mode 100644 index a41dbc0..0000000 --- a/nostr/client/cbc.py +++ /dev/null @@ -1,41 +0,0 @@ - -from Cryptodome import Random -from Cryptodome.Cipher import AES - -plain_text = "This is the text to encrypts" - -# encrypted = "7mH9jq3K9xNfWqIyu9gNpUz8qBvGwsrDJ+ACExdV1DvGgY8q39dkxVKeXD7LWCDrPnoD/ZFHJMRMis8v9lwHfNgJut8EVTMuJJi8oTgJevOBXl+E+bJPwej9hY3k20rgCQistNRtGHUzdWyOv7S1tg==".encode() -# iv = "GzDzqOVShWu3Pl2313FBpQ==".encode() - -key = bytes.fromhex("3aa925cb69eb613e2928f8a18279c78b1dca04541dfd064df2eda66b59880795") - -BLOCK_SIZE = 16 - -class AESCipher(object): - """This class is compatible with crypto.createCipheriv('aes-256-cbc') - - """ - def __init__(self, key=None): - self.key = key - - def pad(self, data): - length = BLOCK_SIZE - (len(data) % BLOCK_SIZE) - return data + (chr(length) * length).encode() - - def unpad(self, data): - return data[: -(data[-1] if type(data[-1]) == int else ord(data[-1]))] - - def encrypt(self, plain_text): - cipher = AES.new(self.key, AES.MODE_CBC) - b = plain_text.encode("UTF-8") - return cipher.iv, cipher.encrypt(self.pad(b)) - - def decrypt(self, iv, enc_text): - cipher = AES.new(self.key, AES.MODE_CBC, iv=iv) - return self.unpad(cipher.decrypt(enc_text).decode("UTF-8")) - -if __name__ == "__main__": - aes = AESCipher(key=key) - iv, enc_text = aes.encrypt(plain_text) - dec_text = aes.decrypt(iv, enc_text) - print(dec_text) \ No newline at end of file diff --git a/nostr/client/client.py b/nostr/client/client.py deleted file mode 100644 index 74aed9d..0000000 --- a/nostr/client/client.py +++ /dev/null @@ -1,165 +0,0 @@ -from typing import * -import ssl -import time -import json -import os -import base64 - -from nostr.event import Event -from nostr.relay_manager import RelayManager -from nostr.message_type import ClientMessageType -from nostr.key import PrivateKey, PublicKey - -from nostr.filter import Filter, Filters -from nostr.event import Event, EventKind -from nostr.relay_manager import RelayManager -from nostr.message_type import ClientMessageType - -# from aes import AESCipher -from . import cbc - - -class NostrClient: - relays = [ - "wss://nostr.zebedee.cloud" - ] # ["wss://nostr.oxtr.dev"] # ["wss://relay.nostr.info"] "wss://nostr-pub.wellorder.net" "ws://91.237.88.218:2700", "wss://nostrrr.bublina.eu.org", ""wss://nostr-relay.freeberty.net"", , "wss://nostr.oxtr.dev", "wss://relay.nostr.info", "wss://nostr-pub.wellorder.net" , "wss://relayer.fiatjaf.com", "wss://nodestr.fmt.wiz.biz/" - relay_manager = RelayManager() - private_key: PrivateKey - public_key: PublicKey - - def __init__(self, privatekey_hex: str = "", relays: List[str] = []): - self.generate_keys(privatekey_hex) - - if len(relays): - self.relays = relays - - for relay in self.relays: - self.relay_manager.add_relay(relay) - self.relay_manager.open_connections( - {"cert_reqs": ssl.CERT_NONE} - ) # NOTE: This disables ssl certificate verification - - def close(self): - self.relay_manager.close_connections() - - def generate_keys(self, privatekey_hex: str = None): - pk = bytes.fromhex(privatekey_hex) if privatekey_hex else None - self.private_key = PrivateKey(pk) - self.public_key = self.private_key.public_key - print(f"Private key: {self.private_key.bech32()} ({self.private_key.hex()})") - print(f"Public key: {self.public_key.bech32()} ({self.public_key.hex()})") - - def post(self, message: str): - event = Event(self.public_key.hex(), message, kind=EventKind.TEXT_NOTE) - event.sign(self.private_key.hex()) - message = json.dumps([ClientMessageType.EVENT, event.to_json_object()]) - print("Publishing message:") - print(message) - self.relay_manager.publish_message(message) - - def get_post(self, sender_publickey: PublicKey): - filters = Filters( - [Filter(authors=[sender_publickey.hex()], kinds=[EventKind.TEXT_NOTE])] - ) - subscription_id = os.urandom(4).hex() - self.relay_manager.add_subscription(subscription_id, filters) - - request = [ClientMessageType.REQUEST, subscription_id] - request.extend(filters.to_json_array()) - message = json.dumps(request) - print("Subscribing to events:") - print(message) - self.relay_manager.publish_message(message) - - message_received = False - while True: - while self.relay_manager.message_pool.has_events(): - event_msg = self.relay_manager.message_pool.get_event() - print(event_msg.event.content) - message_received = True - break - else: - time.sleep(0.1) - - def dm(self, message: str, to_pubkey: PublicKey): - - shared_secret = self.private_key.compute_shared_secret( - to_pubkey.hex() - ) - - # print("shared secret: ", shared_secret.hex()) - # print("plain text:", message) - aes = cbc.AESCipher(key=shared_secret) - iv, enc_text = aes.encrypt(message) - # print("encrypt iv: ", iv) - content = f"{base64.b64encode(enc_text).decode('utf-8')}?iv={base64.b64encode(iv).decode('utf-8')}" - - - event = Event( - self.public_key.hex(), - content, - tags=[["p", to_pubkey.hex()]], - kind=EventKind.ENCRYPTED_DIRECT_MESSAGE, - ) - event.sign(self.private_key.hex()) - event_message = json.dumps([ClientMessageType.EVENT, event.to_json_object()]) - # print("DM message:") - # print(event_message) - - time.sleep(1) - self.relay_manager.publish_message(event_message) - - def get_dm(self, sender_publickey: PublicKey, callback_func=None): - filters = Filters( - [ - Filter( - kinds=[EventKind.ENCRYPTED_DIRECT_MESSAGE], - tags={"#p": [sender_publickey.hex()]}, - ) - ] - ) - subscription_id = os.urandom(4).hex() - self.relay_manager.add_subscription(subscription_id, filters) - - request = [ClientMessageType.REQUEST, subscription_id] - request.extend(filters.to_json_array()) - message = json.dumps(request) - # print("Subscribing to events:") - # print(message) - self.relay_manager.publish_message(message) - - while True: - while self.relay_manager.message_pool.has_events(): - event_msg = self.relay_manager.message_pool.get_event() - - if "?iv=" in event_msg.event.content: - try: - shared_secret = self.private_key.compute_shared_secret( - event_msg.event.public_key - ) - # print("shared secret: ", shared_secret.hex()) - # print("plain text:", message) - aes = cbc.AESCipher(key=shared_secret) - enc_text_b64, iv_b64 = event_msg.event.content.split("?iv=") - iv = base64.decodebytes(iv_b64.encode("utf-8")) - enc_text = base64.decodebytes(enc_text_b64.encode("utf-8")) - # print("decrypt iv: ", iv) - dec_text = aes.decrypt(iv, enc_text) - print(f"From {event_msg.event.public_key[:5]}...: {dec_text}") - if callback_func: - callback_func(dec_text) - except: - pass - else: - print(f"\nFrom {event_msg.event.public_key[:5]}...: {event_msg.event.content}") - break - time.sleep(0.1) - - async def subscribe(self): - while True: - while self.relay_manager.message_pool.has_events(): - event_msg = self.relay_manager.message_pool.get_event() - print(event_msg.event.content) - break - time.sleep(0.1) - diff --git a/nostr/event.py b/nostr/event.py deleted file mode 100644 index 450893e..0000000 --- a/nostr/event.py +++ /dev/null @@ -1,65 +0,0 @@ -import time -import json -from enum import IntEnum -from secp256k1 import PrivateKey, PublicKey -from hashlib import sha256 - -class EventKind(IntEnum): - SET_METADATA = 0 - TEXT_NOTE = 1 - RECOMMEND_RELAY = 2 - CONTACTS = 3 - ENCRYPTED_DIRECT_MESSAGE = 4 - DELETE = 5 - -class Event(): - def __init__( - self, - public_key: str, - content: str, - created_at: int=int(time.time()), - kind: int=EventKind.TEXT_NOTE, - tags: "list[list[str]]"=[], - id: str=None, - signature: str=None) -> None: - if not isinstance(content, str): - raise TypeError("Argument 'content' must be of type str") - - self.id = id if not id is None else Event.compute_id(public_key, created_at, kind, tags, content) - self.public_key = public_key - self.content = content - self.created_at = created_at - self.kind = kind - self.tags = tags - self.signature = signature - - @staticmethod - def serialize(public_key: str, created_at: int, kind: int, tags: "list[list[str]]", content: str) -> bytes: - data = [0, public_key, created_at, kind, tags, content] - data_str = json.dumps(data, separators=(',', ':'), ensure_ascii=False) - return data_str.encode() - - @staticmethod - def compute_id(public_key: str, created_at: int, kind: int, tags: "list[list[str]]", content: str) -> str: - return sha256(Event.serialize(public_key, created_at, kind, tags, content)).hexdigest() - - def sign(self, private_key_hex: str) -> None: - sk = PrivateKey(bytes.fromhex(private_key_hex)) - sig = sk.schnorr_sign(bytes.fromhex(self.id), None, raw=True) - self.signature = sig.hex() - - def verify(self) -> bool: - pub_key = PublicKey(bytes.fromhex("02" + self.public_key), True) # add 02 for schnorr (bip340) - event_id = Event.compute_id(self.public_key, self.created_at, self.kind, self.tags, self.content) - return pub_key.schnorr_verify(bytes.fromhex(event_id), bytes.fromhex(self.signature), None, raw=True) - - def to_json_object(self) -> dict: - return { - "id": self.id, - "pubkey": self.public_key, - "created_at": self.created_at, - "kind": self.kind, - "tags": self.tags, - "content": self.content, - "sig": self.signature - } diff --git a/nostr/filter.py b/nostr/filter.py deleted file mode 100644 index 32b94a3..0000000 --- a/nostr/filter.py +++ /dev/null @@ -1,81 +0,0 @@ -from collections import UserList -from .event import Event - - -class Filter: - def __init__( - self, - ids: "list[str]" = None, - kinds: "list[int]" = None, - authors: "list[str]" = None, - since: int = None, - until: int = None, - tags: "dict[str, list[str]]" = None, - limit: int = None, - ) -> None: - self.IDs = ids - self.kinds = kinds - self.authors = authors - self.since = since - self.until = until - self.tags = tags - self.limit = limit - - def matches(self, event: Event) -> bool: - if self.IDs != None and event.id not in self.IDs: - return False - if self.kinds != None and event.kind not in self.kinds: - return False - if self.authors != None and event.public_key not in self.authors: - return False - if self.since != None and event.created_at < self.since: - return False - if self.until != None and event.created_at > self.until: - return False - if self.tags != None and len(event.tags) == 0: - return False - if self.tags != None: - e_tag_identifiers = [e_tag[0] for e_tag in event.tags] - for f_tag, f_tag_values in self.tags.items(): - if f_tag[1:] not in e_tag_identifiers: - return False - for e_tag in event.tags: - if e_tag[1] not in f_tag_values: - return False - - return True - - def to_json_object(self) -> dict: - res = {} - if self.IDs != None: - res["ids"] = self.IDs - if self.kinds != None: - res["kinds"] = self.kinds - if self.authors != None: - res["authors"] = self.authors - if self.since != None: - res["since"] = self.since - if self.until != None: - res["until"] = self.until - if self.tags != None: - for tag, values in self.tags.items(): - res[tag] = values - if self.limit != None: - res["limit"] = self.limit - - return res - - -class Filters(UserList): - def __init__(self, initlist: "list[Filter]" = []) -> None: - super().__init__(initlist) - self.data: "list[Filter]" - - def match(self, event: Event): - for filter in self.data: - if filter.matches(event): - return True - return False - - def to_json_array(self) -> list: - return [filter.to_json_object() for filter in self.data] diff --git a/nostr/key.py b/nostr/key.py deleted file mode 100644 index 9449c00..0000000 --- a/nostr/key.py +++ /dev/null @@ -1,86 +0,0 @@ -import secrets -import base64 -import secp256k1 -from cffi import FFI -from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes -from cryptography.hazmat.primitives import padding -from . import bech32 - -class PublicKey: - def __init__(self, raw_bytes: bytes) -> None: - self.raw_bytes = raw_bytes - - def bech32(self) -> str: - converted_bits = bech32.convertbits(self.raw_bytes, 8, 5) - return bech32.bech32_encode("npub", converted_bits, bech32.Encoding.BECH32) - - def hex(self) -> str: - return self.raw_bytes.hex() - - def verify_signed_message_hash(self, hash: str, sig: str) -> bool: - pk = secp256k1.PublicKey(b"\x02" + self.raw_bytes, True) - return pk.schnorr_verify(bytes.fromhex(hash), bytes.fromhex(sig), None, True) - -class PrivateKey: - def __init__(self, raw_secret: bytes=None) -> None: - if not raw_secret is None: - self.raw_secret = raw_secret - else: - self.raw_secret = secrets.token_bytes(32) - - sk = secp256k1.PrivateKey(self.raw_secret) - self.public_key = PublicKey(sk.pubkey.serialize()[1:]) - - def bech32(self) -> str: - converted_bits = bech32.convertbits(self.raw_secret, 8, 5) - return bech32.bech32_encode("nsec", converted_bits, bech32.Encoding.BECH32) - - def hex(self) -> str: - return self.raw_secret.hex() - - def tweak_add(self, scalar: bytes) -> bytes: - sk = secp256k1.PrivateKey(self.raw_secret) - return sk.tweak_add(scalar) - - def compute_shared_secret(self, public_key_hex: str) -> bytes: - pk = secp256k1.PublicKey(bytes.fromhex("02" + public_key_hex), True) - return pk.ecdh(self.raw_secret, hashfn=copy_x) - - def encrypt_message(self, message: str, public_key_hex: str) -> str: - padder = padding.PKCS7(128).padder() - padded_data = padder.update(message.encode()) + padder.finalize() - - iv = secrets.token_bytes(16) - cipher = Cipher(algorithms.AES(self.compute_shared_secret(public_key_hex)), modes.CBC(iv)) - - encryptor = cipher.encryptor() - encrypted_message = encryptor.update(padded_data) + encryptor.finalize() - - return f"{base64.b64encode(encrypted_message).decode()}?iv={base64.b64encode(iv).decode()}" - - def decrypt_message(self, encoded_message: str, public_key_hex: str) -> str: - encoded_data = encoded_message.split('?iv=') - encoded_content, encoded_iv = encoded_data[0], encoded_data[1] - - iv = base64.b64decode(encoded_iv) - cipher = Cipher(algorithms.AES(self.compute_shared_secret(public_key_hex)), modes.CBC(iv)) - encrypted_content = base64.b64decode(encoded_content) - - decryptor = cipher.decryptor() - decrypted_message = decryptor.update(encrypted_content) + decryptor.finalize() - - unpadder = padding.PKCS7(128).unpadder() - unpadded_data = unpadder.update(decrypted_message) + unpadder.finalize() - - return unpadded_data.decode() - - def sign_message_hash(self, hash: bytes) -> str: - sk = secp256k1.PrivateKey(self.raw_secret) - sig = sk.schnorr_sign(hash, None, raw=True) - return sig.hex() - -ffi = FFI() -@ffi.callback("int (unsigned char *, const unsigned char *, const unsigned char *, void *)") -def copy_x(output, x32, y32, data): - ffi.memmove(output, x32, 32) - return 1 \ No newline at end of file diff --git a/nostr/message_pool.py b/nostr/message_pool.py deleted file mode 100644 index 472e31e..0000000 --- a/nostr/message_pool.py +++ /dev/null @@ -1,78 +0,0 @@ -import json -from queue import Queue -from threading import Lock -from .message_type import RelayMessageType -from .event import Event - - -class EventMessage: - def __init__(self, event: Event, subscription_id: str, url: str) -> None: - self.event = event - self.subscription_id = subscription_id - self.url = url - - -class NoticeMessage: - def __init__(self, content: str, url: str) -> None: - self.content = content - self.url = url - - -class EndOfStoredEventsMessage: - def __init__(self, subscription_id: str, url: str) -> None: - self.subscription_id = subscription_id - self.url = url - - -class MessagePool: - def __init__(self) -> None: - self.events: Queue[EventMessage] = Queue() - self.notices: Queue[NoticeMessage] = Queue() - self.eose_notices: Queue[EndOfStoredEventsMessage] = Queue() - self._unique_events: set = set() - self.lock: Lock = Lock() - - def add_message(self, message: str, url: str): - self._process_message(message, url) - - def get_event(self): - return self.events.get() - - def get_notice(self): - return self.notices.get() - - def get_eose_notice(self): - return self.eose_notices.get() - - def has_events(self): - return self.events.qsize() > 0 - - def has_notices(self): - return self.notices.qsize() > 0 - - def has_eose_notices(self): - return self.eose_notices.qsize() > 0 - - def _process_message(self, message: str, url: str): - message_json = json.loads(message) - message_type = message_json[0] - if message_type == RelayMessageType.EVENT: - subscription_id = message_json[1] - e = message_json[2] - event = Event( - e["pubkey"], - e["content"], - e["created_at"], - e["kind"], - e["tags"], - e["id"], - e["sig"], - ) - with self.lock: - if not event.id in self._unique_events: - self.events.put(EventMessage(event, subscription_id, url)) - self._unique_events.add(event.id) - elif message_type == RelayMessageType.NOTICE: - self.notices.put(NoticeMessage(message_json[1], url)) - elif message_type == RelayMessageType.END_OF_STORED_EVENTS: - self.eose_notices.put(EndOfStoredEventsMessage(message_json[1], url)) diff --git a/nostr/message_type.py b/nostr/message_type.py deleted file mode 100644 index 3f5206b..0000000 --- a/nostr/message_type.py +++ /dev/null @@ -1,15 +0,0 @@ -class ClientMessageType: - EVENT = "EVENT" - REQUEST = "REQ" - CLOSE = "CLOSE" - -class RelayMessageType: - EVENT = "EVENT" - NOTICE = "NOTICE" - END_OF_STORED_EVENTS = "EOSE" - - @staticmethod - def is_valid(type: str) -> bool: - if type == RelayMessageType.EVENT or type == RelayMessageType.NOTICE or type == RelayMessageType.END_OF_STORED_EVENTS: - return True - return False \ No newline at end of file diff --git a/nostr/nostr/__init__.py b/nostr/nostr/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/nostr/nostr/bech32.py b/nostr/nostr/bech32.py deleted file mode 100644 index b068de7..0000000 --- a/nostr/nostr/bech32.py +++ /dev/null @@ -1,137 +0,0 @@ -# Copyright (c) 2017, 2020 Pieter Wuille -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -"""Reference implementation for Bech32/Bech32m and segwit addresses.""" - - -from enum import Enum - -class Encoding(Enum): - """Enumeration type to list the various supported encodings.""" - BECH32 = 1 - BECH32M = 2 - -CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" -BECH32M_CONST = 0x2bc830a3 - -def bech32_polymod(values): - """Internal function that computes the Bech32 checksum.""" - generator = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3] - chk = 1 - for value in values: - top = chk >> 25 - chk = (chk & 0x1ffffff) << 5 ^ value - for i in range(5): - chk ^= generator[i] if ((top >> i) & 1) else 0 - return chk - - -def bech32_hrp_expand(hrp): - """Expand the HRP into values for checksum computation.""" - return [ord(x) >> 5 for x in hrp] + [0] + [ord(x) & 31 for x in hrp] - - -def bech32_verify_checksum(hrp, data): - """Verify a checksum given HRP and converted data characters.""" - const = bech32_polymod(bech32_hrp_expand(hrp) + data) - if const == 1: - return Encoding.BECH32 - if const == BECH32M_CONST: - return Encoding.BECH32M - return None - -def bech32_create_checksum(hrp, data, spec): - """Compute the checksum values given HRP and data.""" - values = bech32_hrp_expand(hrp) + data - const = BECH32M_CONST if spec == Encoding.BECH32M else 1 - polymod = bech32_polymod(values + [0, 0, 0, 0, 0, 0]) ^ const - return [(polymod >> 5 * (5 - i)) & 31 for i in range(6)] - - -def bech32_encode(hrp, data, spec): - """Compute a Bech32 string given HRP and data values.""" - combined = data + bech32_create_checksum(hrp, data, spec) - return hrp + '1' + ''.join([CHARSET[d] for d in combined]) - -def bech32_decode(bech): - """Validate a Bech32/Bech32m string, and determine HRP and data.""" - if ((any(ord(x) < 33 or ord(x) > 126 for x in bech)) or - (bech.lower() != bech and bech.upper() != bech)): - return (None, None, None) - bech = bech.lower() - pos = bech.rfind('1') - if pos < 1 or pos + 7 > len(bech) or len(bech) > 90: - return (None, None, None) - if not all(x in CHARSET for x in bech[pos+1:]): - return (None, None, None) - hrp = bech[:pos] - data = [CHARSET.find(x) for x in bech[pos+1:]] - spec = bech32_verify_checksum(hrp, data) - if spec is None: - return (None, None, None) - return (hrp, data[:-6], spec) - -def convertbits(data, frombits, tobits, pad=True): - """General power-of-2 base conversion.""" - acc = 0 - bits = 0 - ret = [] - maxv = (1 << tobits) - 1 - max_acc = (1 << (frombits + tobits - 1)) - 1 - for value in data: - if value < 0 or (value >> frombits): - return None - acc = ((acc << frombits) | value) & max_acc - bits += frombits - while bits >= tobits: - bits -= tobits - ret.append((acc >> bits) & maxv) - if pad: - if bits: - ret.append((acc << (tobits - bits)) & maxv) - elif bits >= frombits or ((acc << (tobits - bits)) & maxv): - return None - return ret - - -def decode(hrp, addr): - """Decode a segwit address.""" - hrpgot, data, spec = bech32_decode(addr) - if hrpgot != hrp: - return (None, None) - decoded = convertbits(data[1:], 5, 8, False) - if decoded is None or len(decoded) < 2 or len(decoded) > 40: - return (None, None) - if data[0] > 16: - return (None, None) - if data[0] == 0 and len(decoded) != 20 and len(decoded) != 32: - return (None, None) - if data[0] == 0 and spec != Encoding.BECH32 or data[0] != 0 and spec != Encoding.BECH32M: - return (None, None) - return (data[0], decoded) - - -def encode(hrp, witver, witprog): - """Encode a segwit address.""" - spec = Encoding.BECH32 if witver == 0 else Encoding.BECH32M - ret = bech32_encode(hrp, [witver] + convertbits(witprog, 8, 5), spec) - if decode(hrp, ret) == (None, None): - return None - return ret diff --git a/nostr/nostr/client/__init__.py b/nostr/nostr/client/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/nostr/nostr/client/aes.py b/nostr/nostr/client/aes.py deleted file mode 100644 index 9d20224..0000000 --- a/nostr/nostr/client/aes.py +++ /dev/null @@ -1,86 +0,0 @@ -import base64 -import getpass -from hashlib import md5 - -from Cryptodome import Random -from Cryptodome.Cipher import AES - -BLOCK_SIZE = 16 - -import getpass - - -class AESCipher(object): - """This class is compatible with crypto-js/aes.js - - Encrypt and decrypt in Javascript using: - import AES from "crypto-js/aes.js"; - import Utf8 from "crypto-js/enc-utf8.js"; - AES.encrypt(decrypted, password).toString() - AES.decrypt(encrypted, password).toString(Utf8); - - """ - - def __init__(self, key=None, description=""): - self.key = key - self.description = description + " " - - def pad(self, data): - length = BLOCK_SIZE - (len(data) % BLOCK_SIZE) - return data + (chr(length) * length).encode() - - def unpad(self, data): - return data[: -(data[-1] if type(data[-1]) == int else ord(data[-1]))] - - @property - def passphrase(self): - passphrase = self.key if self.key is not None else None - if passphrase is None: - passphrase = getpass.getpass(f"Enter {self.description}password:") - return passphrase - - def bytes_to_key(self, data, salt, output=48): - # extended from https://gist.github.com/gsakkis/4546068 - assert len(salt) == 8, len(salt) - data += salt - key = md5(data).digest() - final_key = key - while len(final_key) < output: - key = md5(key + data).digest() - final_key += key - return final_key[:output] - - def decrypt(self, encrypted: str) -> str: # type: ignore - """Decrypts a string using AES-256-CBC.""" - passphrase = self.passphrase - encrypted = base64.b64decode(encrypted) # type: ignore - assert encrypted[0:8] == b"Salted__" - salt = encrypted[8:16] - key_iv = self.bytes_to_key(passphrase.encode(), salt, 32 + 16) - key = key_iv[:32] - iv = key_iv[32:] - aes = AES.new(key, AES.MODE_CBC, iv) - try: - return self.unpad(aes.decrypt(encrypted[16:])).decode() # type: ignore - except UnicodeDecodeError: - raise ValueError("Wrong passphrase") - - def encrypt(self, message: bytes) -> str: - passphrase = self.passphrase - salt = Random.new().read(8) - key_iv = self.bytes_to_key(passphrase.encode(), salt, 32 + 16) - key = key_iv[:32] - iv = key_iv[32:] - aes = AES.new(key, AES.MODE_CBC, iv) - return base64.b64encode( - b"Salted__" + salt + aes.encrypt(self.pad(message)) - ).decode() - - -# # if this file is executed directly, ask for a macaroon and encrypt it -# if __name__ == "__main__": -# macaroon = input("Enter macaroon: ") -# macaroon = load_macaroon(macaroon) -# macaroon = AESCipher(description="encryption").encrypt(macaroon.encode()) -# logger.info("Encrypted macaroon:") -# logger.info(macaroon) diff --git a/nostr/nostr/client/cbc.py b/nostr/nostr/client/cbc.py deleted file mode 100644 index a41dbc0..0000000 --- a/nostr/nostr/client/cbc.py +++ /dev/null @@ -1,41 +0,0 @@ - -from Cryptodome import Random -from Cryptodome.Cipher import AES - -plain_text = "This is the text to encrypts" - -# encrypted = "7mH9jq3K9xNfWqIyu9gNpUz8qBvGwsrDJ+ACExdV1DvGgY8q39dkxVKeXD7LWCDrPnoD/ZFHJMRMis8v9lwHfNgJut8EVTMuJJi8oTgJevOBXl+E+bJPwej9hY3k20rgCQistNRtGHUzdWyOv7S1tg==".encode() -# iv = "GzDzqOVShWu3Pl2313FBpQ==".encode() - -key = bytes.fromhex("3aa925cb69eb613e2928f8a18279c78b1dca04541dfd064df2eda66b59880795") - -BLOCK_SIZE = 16 - -class AESCipher(object): - """This class is compatible with crypto.createCipheriv('aes-256-cbc') - - """ - def __init__(self, key=None): - self.key = key - - def pad(self, data): - length = BLOCK_SIZE - (len(data) % BLOCK_SIZE) - return data + (chr(length) * length).encode() - - def unpad(self, data): - return data[: -(data[-1] if type(data[-1]) == int else ord(data[-1]))] - - def encrypt(self, plain_text): - cipher = AES.new(self.key, AES.MODE_CBC) - b = plain_text.encode("UTF-8") - return cipher.iv, cipher.encrypt(self.pad(b)) - - def decrypt(self, iv, enc_text): - cipher = AES.new(self.key, AES.MODE_CBC, iv=iv) - return self.unpad(cipher.decrypt(enc_text).decode("UTF-8")) - -if __name__ == "__main__": - aes = AESCipher(key=key) - iv, enc_text = aes.encrypt(plain_text) - dec_text = aes.decrypt(iv, enc_text) - print(dec_text) \ No newline at end of file diff --git a/nostr/nostr/client/client.py b/nostr/nostr/client/client.py deleted file mode 100644 index 68ec69d..0000000 --- a/nostr/nostr/client/client.py +++ /dev/null @@ -1,162 +0,0 @@ -from typing import * -import ssl -import time -import json -import os -import base64 - -from nostr.event import Event -from nostr.relay_manager import RelayManager -from nostr.message_type import ClientMessageType -from nostr.key import PrivateKey, PublicKey - -from nostr.filter import Filter, Filters -from nostr.event import Event, EventKind -from nostr.relay_manager import RelayManager -from nostr.message_type import ClientMessageType - -# from aes import AESCipher -from . import cbc - - -class NostrClient: - relays = [ - "wss://nostr.zebedee.cloud", - "wss://nostr-relay.digitalmob.ro", - ] # ["wss://nostr.oxtr.dev"] # ["wss://relay.nostr.info"] "wss://nostr-pub.wellorder.net" "ws://91.237.88.218:2700", "wss://nostrrr.bublina.eu.org", ""wss://nostr-relay.freeberty.net"", , "wss://nostr.oxtr.dev", "wss://relay.nostr.info", "wss://nostr-pub.wellorder.net" , "wss://relayer.fiatjaf.com", "wss://nodestr.fmt.wiz.biz/", "wss://no.str.cr", "wss://nostr-relay.digitalmob.ro" - relay_manager = RelayManager() - private_key: PrivateKey - public_key: PublicKey - - def __init__(self, privatekey_hex: str = "", relays: List[str] = []): - self.generate_keys(privatekey_hex) - - if len(relays): - self.relays = relays - - for relay in self.relays: - self.relay_manager.add_relay(relay) - self.relay_manager.open_connections( - {"cert_reqs": ssl.CERT_NONE} - ) # NOTE: This disables ssl certificate verification - - def close(self): - self.relay_manager.close_connections() - - def generate_keys(self, privatekey_hex: str = None): - pk = bytes.fromhex(privatekey_hex) if privatekey_hex else None - self.private_key = PrivateKey(pk) - self.public_key = self.private_key.public_key - print(f"Private key: {self.private_key.bech32()} ({self.private_key.hex()})") - print(f"Public key: {self.public_key.bech32()} ({self.public_key.hex()})") - - def post(self, message: str): - event = Event(self.public_key.hex(), message, kind=EventKind.TEXT_NOTE) - event.sign(self.private_key.hex()) - message = json.dumps([ClientMessageType.EVENT, event.to_json_object()]) - # print("Publishing message:") - # print(message) - self.relay_manager.publish_message(message) - - def get_post(self, sender_publickey: PublicKey): - filters = Filters( - [Filter(authors=[sender_publickey.hex()], kinds=[EventKind.TEXT_NOTE])] - ) - subscription_id = os.urandom(4).hex() - self.relay_manager.add_subscription(subscription_id, filters) - - request = [ClientMessageType.REQUEST, subscription_id] - request.extend(filters.to_json_array()) - message = json.dumps(request) - # print("Subscribing to events:") - # print(message) - self.relay_manager.publish_message(message) - - message_received = False - while True: - while self.relay_manager.message_pool.has_events(): - event_msg = self.relay_manager.message_pool.get_event() - print(event_msg.event.content) - message_received = True - break - else: - time.sleep(0.1) - - def dm(self, message: str, to_pubkey: PublicKey): - - shared_secret = self.private_key.compute_shared_secret(to_pubkey.hex()) - - # print("shared secret: ", shared_secret.hex()) - # print("plain text:", message) - aes = cbc.AESCipher(key=shared_secret) - iv, enc_text = aes.encrypt(message) - # print("encrypt iv: ", iv) - content = f"{base64.b64encode(enc_text).decode('utf-8')}?iv={base64.b64encode(iv).decode('utf-8')}" - - event = Event( - self.public_key.hex(), - content, - tags=[["p", to_pubkey.hex()]], - kind=EventKind.ENCRYPTED_DIRECT_MESSAGE, - ) - event.sign(self.private_key.hex()) - event_message = json.dumps([ClientMessageType.EVENT, event.to_json_object()]) - # print("DM message:") - # print(event_message) - - time.sleep(1) - self.relay_manager.publish_message(event_message) - - def get_dm(self, sender_publickey: PublicKey, callback_func=None): - filters = Filters( - [ - Filter( - kinds=[EventKind.ENCRYPTED_DIRECT_MESSAGE], - tags={"#p": [sender_publickey.hex()]}, - ) - ] - ) - subscription_id = os.urandom(4).hex() - self.relay_manager.add_subscription(subscription_id, filters) - - request = [ClientMessageType.REQUEST, subscription_id] - request.extend(filters.to_json_array()) - message = json.dumps(request) - # print("Subscribing to events:") - # print(message) - self.relay_manager.publish_message(message) - - while True: - while self.relay_manager.message_pool.has_events(): - event_msg = self.relay_manager.message_pool.get_event() - - if "?iv=" in event_msg.event.content: - try: - shared_secret = self.private_key.compute_shared_secret( - event_msg.event.public_key - ) - # print("shared secret: ", shared_secret.hex()) - # print("plain text:", message) - aes = cbc.AESCipher(key=shared_secret) - enc_text_b64, iv_b64 = event_msg.event.content.split("?iv=") - iv = base64.decodebytes(iv_b64.encode("utf-8")) - enc_text = base64.decodebytes(enc_text_b64.encode("utf-8")) - # print("decrypt iv: ", iv) - dec_text = aes.decrypt(iv, enc_text) - # print(f"From {event_msg.event.public_key[:5]}...: {dec_text}") - if callback_func: - callback_func(event_msg.event, dec_text) - except: - pass - # else: - # print(f"\nFrom {event_msg.event.public_key[:5]}...: {event_msg.event.content}") - break - time.sleep(0.1) - - async def subscribe(self): - while True: - while self.relay_manager.message_pool.has_events(): - event_msg = self.relay_manager.message_pool.get_event() - print(event_msg.event.content) - break - time.sleep(0.1) diff --git a/nostr/nostr/event.py b/nostr/nostr/event.py deleted file mode 100644 index 450893e..0000000 --- a/nostr/nostr/event.py +++ /dev/null @@ -1,65 +0,0 @@ -import time -import json -from enum import IntEnum -from secp256k1 import PrivateKey, PublicKey -from hashlib import sha256 - -class EventKind(IntEnum): - SET_METADATA = 0 - TEXT_NOTE = 1 - RECOMMEND_RELAY = 2 - CONTACTS = 3 - ENCRYPTED_DIRECT_MESSAGE = 4 - DELETE = 5 - -class Event(): - def __init__( - self, - public_key: str, - content: str, - created_at: int=int(time.time()), - kind: int=EventKind.TEXT_NOTE, - tags: "list[list[str]]"=[], - id: str=None, - signature: str=None) -> None: - if not isinstance(content, str): - raise TypeError("Argument 'content' must be of type str") - - self.id = id if not id is None else Event.compute_id(public_key, created_at, kind, tags, content) - self.public_key = public_key - self.content = content - self.created_at = created_at - self.kind = kind - self.tags = tags - self.signature = signature - - @staticmethod - def serialize(public_key: str, created_at: int, kind: int, tags: "list[list[str]]", content: str) -> bytes: - data = [0, public_key, created_at, kind, tags, content] - data_str = json.dumps(data, separators=(',', ':'), ensure_ascii=False) - return data_str.encode() - - @staticmethod - def compute_id(public_key: str, created_at: int, kind: int, tags: "list[list[str]]", content: str) -> str: - return sha256(Event.serialize(public_key, created_at, kind, tags, content)).hexdigest() - - def sign(self, private_key_hex: str) -> None: - sk = PrivateKey(bytes.fromhex(private_key_hex)) - sig = sk.schnorr_sign(bytes.fromhex(self.id), None, raw=True) - self.signature = sig.hex() - - def verify(self) -> bool: - pub_key = PublicKey(bytes.fromhex("02" + self.public_key), True) # add 02 for schnorr (bip340) - event_id = Event.compute_id(self.public_key, self.created_at, self.kind, self.tags, self.content) - return pub_key.schnorr_verify(bytes.fromhex(event_id), bytes.fromhex(self.signature), None, raw=True) - - def to_json_object(self) -> dict: - return { - "id": self.id, - "pubkey": self.public_key, - "created_at": self.created_at, - "kind": self.kind, - "tags": self.tags, - "content": self.content, - "sig": self.signature - } diff --git a/nostr/nostr/filter.py b/nostr/nostr/filter.py deleted file mode 100644 index 32b94a3..0000000 --- a/nostr/nostr/filter.py +++ /dev/null @@ -1,81 +0,0 @@ -from collections import UserList -from .event import Event - - -class Filter: - def __init__( - self, - ids: "list[str]" = None, - kinds: "list[int]" = None, - authors: "list[str]" = None, - since: int = None, - until: int = None, - tags: "dict[str, list[str]]" = None, - limit: int = None, - ) -> None: - self.IDs = ids - self.kinds = kinds - self.authors = authors - self.since = since - self.until = until - self.tags = tags - self.limit = limit - - def matches(self, event: Event) -> bool: - if self.IDs != None and event.id not in self.IDs: - return False - if self.kinds != None and event.kind not in self.kinds: - return False - if self.authors != None and event.public_key not in self.authors: - return False - if self.since != None and event.created_at < self.since: - return False - if self.until != None and event.created_at > self.until: - return False - if self.tags != None and len(event.tags) == 0: - return False - if self.tags != None: - e_tag_identifiers = [e_tag[0] for e_tag in event.tags] - for f_tag, f_tag_values in self.tags.items(): - if f_tag[1:] not in e_tag_identifiers: - return False - for e_tag in event.tags: - if e_tag[1] not in f_tag_values: - return False - - return True - - def to_json_object(self) -> dict: - res = {} - if self.IDs != None: - res["ids"] = self.IDs - if self.kinds != None: - res["kinds"] = self.kinds - if self.authors != None: - res["authors"] = self.authors - if self.since != None: - res["since"] = self.since - if self.until != None: - res["until"] = self.until - if self.tags != None: - for tag, values in self.tags.items(): - res[tag] = values - if self.limit != None: - res["limit"] = self.limit - - return res - - -class Filters(UserList): - def __init__(self, initlist: "list[Filter]" = []) -> None: - super().__init__(initlist) - self.data: "list[Filter]" - - def match(self, event: Event): - for filter in self.data: - if filter.matches(event): - return True - return False - - def to_json_array(self) -> list: - return [filter.to_json_object() for filter in self.data] diff --git a/nostr/nostr/key.py b/nostr/nostr/key.py deleted file mode 100644 index 9449c00..0000000 --- a/nostr/nostr/key.py +++ /dev/null @@ -1,86 +0,0 @@ -import secrets -import base64 -import secp256k1 -from cffi import FFI -from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes -from cryptography.hazmat.primitives import padding -from . import bech32 - -class PublicKey: - def __init__(self, raw_bytes: bytes) -> None: - self.raw_bytes = raw_bytes - - def bech32(self) -> str: - converted_bits = bech32.convertbits(self.raw_bytes, 8, 5) - return bech32.bech32_encode("npub", converted_bits, bech32.Encoding.BECH32) - - def hex(self) -> str: - return self.raw_bytes.hex() - - def verify_signed_message_hash(self, hash: str, sig: str) -> bool: - pk = secp256k1.PublicKey(b"\x02" + self.raw_bytes, True) - return pk.schnorr_verify(bytes.fromhex(hash), bytes.fromhex(sig), None, True) - -class PrivateKey: - def __init__(self, raw_secret: bytes=None) -> None: - if not raw_secret is None: - self.raw_secret = raw_secret - else: - self.raw_secret = secrets.token_bytes(32) - - sk = secp256k1.PrivateKey(self.raw_secret) - self.public_key = PublicKey(sk.pubkey.serialize()[1:]) - - def bech32(self) -> str: - converted_bits = bech32.convertbits(self.raw_secret, 8, 5) - return bech32.bech32_encode("nsec", converted_bits, bech32.Encoding.BECH32) - - def hex(self) -> str: - return self.raw_secret.hex() - - def tweak_add(self, scalar: bytes) -> bytes: - sk = secp256k1.PrivateKey(self.raw_secret) - return sk.tweak_add(scalar) - - def compute_shared_secret(self, public_key_hex: str) -> bytes: - pk = secp256k1.PublicKey(bytes.fromhex("02" + public_key_hex), True) - return pk.ecdh(self.raw_secret, hashfn=copy_x) - - def encrypt_message(self, message: str, public_key_hex: str) -> str: - padder = padding.PKCS7(128).padder() - padded_data = padder.update(message.encode()) + padder.finalize() - - iv = secrets.token_bytes(16) - cipher = Cipher(algorithms.AES(self.compute_shared_secret(public_key_hex)), modes.CBC(iv)) - - encryptor = cipher.encryptor() - encrypted_message = encryptor.update(padded_data) + encryptor.finalize() - - return f"{base64.b64encode(encrypted_message).decode()}?iv={base64.b64encode(iv).decode()}" - - def decrypt_message(self, encoded_message: str, public_key_hex: str) -> str: - encoded_data = encoded_message.split('?iv=') - encoded_content, encoded_iv = encoded_data[0], encoded_data[1] - - iv = base64.b64decode(encoded_iv) - cipher = Cipher(algorithms.AES(self.compute_shared_secret(public_key_hex)), modes.CBC(iv)) - encrypted_content = base64.b64decode(encoded_content) - - decryptor = cipher.decryptor() - decrypted_message = decryptor.update(encrypted_content) + decryptor.finalize() - - unpadder = padding.PKCS7(128).unpadder() - unpadded_data = unpadder.update(decrypted_message) + unpadder.finalize() - - return unpadded_data.decode() - - def sign_message_hash(self, hash: bytes) -> str: - sk = secp256k1.PrivateKey(self.raw_secret) - sig = sk.schnorr_sign(hash, None, raw=True) - return sig.hex() - -ffi = FFI() -@ffi.callback("int (unsigned char *, const unsigned char *, const unsigned char *, void *)") -def copy_x(output, x32, y32, data): - ffi.memmove(output, x32, 32) - return 1 \ No newline at end of file diff --git a/nostr/nostr/message_pool.py b/nostr/nostr/message_pool.py deleted file mode 100644 index 472e31e..0000000 --- a/nostr/nostr/message_pool.py +++ /dev/null @@ -1,78 +0,0 @@ -import json -from queue import Queue -from threading import Lock -from .message_type import RelayMessageType -from .event import Event - - -class EventMessage: - def __init__(self, event: Event, subscription_id: str, url: str) -> None: - self.event = event - self.subscription_id = subscription_id - self.url = url - - -class NoticeMessage: - def __init__(self, content: str, url: str) -> None: - self.content = content - self.url = url - - -class EndOfStoredEventsMessage: - def __init__(self, subscription_id: str, url: str) -> None: - self.subscription_id = subscription_id - self.url = url - - -class MessagePool: - def __init__(self) -> None: - self.events: Queue[EventMessage] = Queue() - self.notices: Queue[NoticeMessage] = Queue() - self.eose_notices: Queue[EndOfStoredEventsMessage] = Queue() - self._unique_events: set = set() - self.lock: Lock = Lock() - - def add_message(self, message: str, url: str): - self._process_message(message, url) - - def get_event(self): - return self.events.get() - - def get_notice(self): - return self.notices.get() - - def get_eose_notice(self): - return self.eose_notices.get() - - def has_events(self): - return self.events.qsize() > 0 - - def has_notices(self): - return self.notices.qsize() > 0 - - def has_eose_notices(self): - return self.eose_notices.qsize() > 0 - - def _process_message(self, message: str, url: str): - message_json = json.loads(message) - message_type = message_json[0] - if message_type == RelayMessageType.EVENT: - subscription_id = message_json[1] - e = message_json[2] - event = Event( - e["pubkey"], - e["content"], - e["created_at"], - e["kind"], - e["tags"], - e["id"], - e["sig"], - ) - with self.lock: - if not event.id in self._unique_events: - self.events.put(EventMessage(event, subscription_id, url)) - self._unique_events.add(event.id) - elif message_type == RelayMessageType.NOTICE: - self.notices.put(NoticeMessage(message_json[1], url)) - elif message_type == RelayMessageType.END_OF_STORED_EVENTS: - self.eose_notices.put(EndOfStoredEventsMessage(message_json[1], url)) diff --git a/nostr/nostr/message_type.py b/nostr/nostr/message_type.py deleted file mode 100644 index 3f5206b..0000000 --- a/nostr/nostr/message_type.py +++ /dev/null @@ -1,15 +0,0 @@ -class ClientMessageType: - EVENT = "EVENT" - REQUEST = "REQ" - CLOSE = "CLOSE" - -class RelayMessageType: - EVENT = "EVENT" - NOTICE = "NOTICE" - END_OF_STORED_EVENTS = "EOSE" - - @staticmethod - def is_valid(type: str) -> bool: - if type == RelayMessageType.EVENT or type == RelayMessageType.NOTICE or type == RelayMessageType.END_OF_STORED_EVENTS: - return True - return False \ No newline at end of file diff --git a/nostr/nostr/relay.py b/nostr/nostr/relay.py deleted file mode 100644 index ad01ff6..0000000 --- a/nostr/nostr/relay.py +++ /dev/null @@ -1,123 +0,0 @@ -import json -from threading import Lock -from websocket import WebSocketApp -from .event import Event -from .filter import Filters -from .message_pool import MessagePool -from .message_type import RelayMessageType -from .subscription import Subscription - - -class RelayPolicy: - def __init__(self, should_read: bool = True, should_write: bool = True) -> None: - self.should_read = should_read - self.should_write = should_write - - def to_json_object(self) -> dict[str, bool]: - return {"read": self.should_read, "write": self.should_write} - - -class Relay: - def __init__( - self, - url: str, - policy: RelayPolicy, - message_pool: MessagePool, - subscriptions: dict[str, Subscription] = {}, - ) -> None: - self.url = url - self.policy = policy - self.message_pool = message_pool - self.subscriptions = subscriptions - self.lock = Lock() - self.ws = WebSocketApp( - url, - on_open=self._on_open, - on_message=self._on_message, - on_error=self._on_error, - on_close=self._on_close, - ) - - def connect(self, ssl_options: dict = None): - self.ws.run_forever(sslopt=ssl_options) - - def close(self): - self.ws.close() - - def publish(self, message: str): - self.ws.send(message) - - def add_subscription(self, id, filters: Filters): - with self.lock: - self.subscriptions[id] = Subscription(id, filters) - - def close_subscription(self, id: str) -> None: - with self.lock: - self.subscriptions.pop(id) - - def update_subscription(self, id: str, filters: Filters) -> None: - with self.lock: - subscription = self.subscriptions[id] - subscription.filters = filters - - def to_json_object(self) -> dict: - return { - "url": self.url, - "policy": self.policy.to_json_object(), - "subscriptions": [ - subscription.to_json_object() - for subscription in self.subscriptions.values() - ], - } - - def _on_open(self, class_obj): - pass - - def _on_close(self, class_obj, status_code, message): - pass - - def _on_message(self, class_obj, message: str): - if self._is_valid_message(message): - self.message_pool.add_message(message, self.url) - - def _on_error(self, class_obj, error): - pass - - def _is_valid_message(self, message: str) -> bool: - message = message.strip("\n") - if not message or message[0] != "[" or message[-1] != "]": - return False - - message_json = json.loads(message) - message_type = message_json[0] - if not RelayMessageType.is_valid(message_type): - return False - if message_type == RelayMessageType.EVENT: - if not len(message_json) == 3: - return False - - subscription_id = message_json[1] - with self.lock: - if subscription_id not in self.subscriptions: - return False - - e = message_json[2] - event = Event( - e["pubkey"], - e["content"], - e["created_at"], - e["kind"], - e["tags"], - e["id"], - e["sig"], - ) - if not event.verify(): - return False - - with self.lock: - subscription = self.subscriptions[subscription_id] - - if not subscription.filters.match(event): - return False - - return True diff --git a/nostr/nostr/relay_manager.py b/nostr/nostr/relay_manager.py deleted file mode 100644 index e4d177e..0000000 --- a/nostr/nostr/relay_manager.py +++ /dev/null @@ -1,43 +0,0 @@ -import threading -from .filter import Filters -from .message_pool import MessagePool -from .relay import Relay, RelayPolicy - -class RelayManager: - def __init__(self) -> None: - self.relays: dict[str, Relay] = {} - self.message_pool = MessagePool() - - def add_relay(self, url: str, read: bool=True, write: bool=True, subscriptions={}): - policy = RelayPolicy(read, write) - relay = Relay(url, policy, self.message_pool, subscriptions) - self.relays[url] = relay - - def remove_relay(self, url: str): - self.relays.pop(url) - - def add_subscription(self, id: str, filters: Filters): - for relay in self.relays.values(): - relay.add_subscription(id, filters) - - def close_subscription(self, id: str): - for relay in self.relays.values(): - relay.close_subscription(id) - - def open_connections(self, ssl_options: dict=None): - for relay in self.relays.values(): - threading.Thread( - target=relay.connect, - args=(ssl_options,), - name=f"{relay.url}-thread" - ).start() - - def close_connections(self): - for relay in self.relays.values(): - relay.close() - - def publish_message(self, message: str): - for relay in self.relays.values(): - if relay.policy.should_write: - relay.publish(message) - diff --git a/nostr/nostr/subscription.py b/nostr/nostr/subscription.py deleted file mode 100644 index 7afba20..0000000 --- a/nostr/nostr/subscription.py +++ /dev/null @@ -1,12 +0,0 @@ -from .filter import Filters - -class Subscription: - def __init__(self, id: str, filters: Filters=None) -> None: - self.id = id - self.filters = filters - - def to_json_object(self): - return { - "id": self.id, - "filters": self.filters.to_json_array() - } diff --git a/nostr/relay.py b/nostr/relay.py deleted file mode 100644 index 16667eb..0000000 --- a/nostr/relay.py +++ /dev/null @@ -1,124 +0,0 @@ -import json -from threading import Lock -from websocket import WebSocketApp -from .event import Event -from .filter import Filters -from .message_pool import MessagePool -from .message_type import RelayMessageType -from .subscription import Subscription - - -class RelayPolicy: - def __init__(self, should_read: bool = True, should_write: bool = True) -> None: - self.should_read = should_read - self.should_write = should_write - - def to_json_object(self) -> dict[str, bool]: - return {"read": self.should_read, "write": self.should_write} - - -class Relay: - def __init__( - self, - url: str, - policy: RelayPolicy, - message_pool: MessagePool, - subscriptions: dict[str, Subscription] = {}, - ) -> None: - self.url = url - self.policy = policy - self.message_pool = message_pool - self.subscriptions = subscriptions - self.lock = Lock() - self.ws = WebSocketApp( - url, - on_open=self._on_open, - on_message=self._on_message, - on_error=self._on_error, - on_close=self._on_close, - ) - - def connect(self, ssl_options: dict = None): - self.ws.run_forever(sslopt=ssl_options) - - def close(self): - self.ws.close() - - def publish(self, message: str): - self.ws.send(message) - - def add_subscription(self, id, filters: Filters): - with self.lock: - self.subscriptions[id] = Subscription(id, filters) - - def close_subscription(self, id: str) -> None: - with self.lock: - self.subscriptions.pop(id) - - def update_subscription(self, id: str, filters: Filters) -> None: - with self.lock: - subscription = self.subscriptions[id] - subscription.filters = filters - - def to_json_object(self) -> dict: - return { - "url": self.url, - "policy": self.policy.to_json_object(), - "subscriptions": [ - subscription.to_json_object() - for subscription in self.subscriptions.values() - ], - } - - def _on_open(self, class_obj): - pass - - def _on_close(self, class_obj, status_code, message): - pass - - def _on_message(self, class_obj, message: str): - if self._is_valid_message(message): - self.message_pool.add_message(message, self.url) - - def _on_error(self, class_obj, error): - # print(error) - pass - - def _is_valid_message(self, message: str) -> bool: - message = message.strip("\n") - if not message or message[0] != "[" or message[-1] != "]": - return False - - message_json = json.loads(message) - message_type = message_json[0] - if not RelayMessageType.is_valid(message_type): - return False - if message_type == RelayMessageType.EVENT: - if not len(message_json) == 3: - return False - - subscription_id = message_json[1] - with self.lock: - if subscription_id not in self.subscriptions: - return False - - e = message_json[2] - event = Event( - e["pubkey"], - e["content"], - e["created_at"], - e["kind"], - e["tags"], - e["id"], - e["sig"], - ) - if not event.verify(): - return False - - with self.lock: - subscription = self.subscriptions[subscription_id] - - if not subscription.filters.match(event): - return False - - return True diff --git a/nostr/relay_manager.py b/nostr/relay_manager.py deleted file mode 100644 index e4d177e..0000000 --- a/nostr/relay_manager.py +++ /dev/null @@ -1,43 +0,0 @@ -import threading -from .filter import Filters -from .message_pool import MessagePool -from .relay import Relay, RelayPolicy - -class RelayManager: - def __init__(self) -> None: - self.relays: dict[str, Relay] = {} - self.message_pool = MessagePool() - - def add_relay(self, url: str, read: bool=True, write: bool=True, subscriptions={}): - policy = RelayPolicy(read, write) - relay = Relay(url, policy, self.message_pool, subscriptions) - self.relays[url] = relay - - def remove_relay(self, url: str): - self.relays.pop(url) - - def add_subscription(self, id: str, filters: Filters): - for relay in self.relays.values(): - relay.add_subscription(id, filters) - - def close_subscription(self, id: str): - for relay in self.relays.values(): - relay.close_subscription(id) - - def open_connections(self, ssl_options: dict=None): - for relay in self.relays.values(): - threading.Thread( - target=relay.connect, - args=(ssl_options,), - name=f"{relay.url}-thread" - ).start() - - def close_connections(self): - for relay in self.relays.values(): - relay.close() - - def publish_message(self, message: str): - for relay in self.relays.values(): - if relay.policy.should_write: - relay.publish(message) - diff --git a/nostr/subscription.py b/nostr/subscription.py deleted file mode 100644 index 7afba20..0000000 --- a/nostr/subscription.py +++ /dev/null @@ -1,12 +0,0 @@ -from .filter import Filters - -class Subscription: - def __init__(self, id: str, filters: Filters=None) -> None: - self.id = id - self.filters = filters - - def to_json_object(self): - return { - "id": self.id, - "filters": self.filters.to_json_array() - } diff --git a/poetry.lock b/poetry.lock index c3cbd9f..f334f30 100644 --- a/poetry.lock +++ b/poetry.lock @@ -18,17 +18,19 @@ trio = ["trio (>=0.16,<0.22)"] [[package]] name = "attrs" -version = "22.1.0" +version = "22.2.0" description = "Classes Without Boilerplate" category = "main" optional = false -python-versions = ">=3.5" +python-versions = ">=3.6" [package.extras] -dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy (>=0.900,!=0.940)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "sphinx", "sphinx-notfound-page", "zope.interface"] -docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"] -tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "zope.interface"] -tests_no_zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"] +cov = ["attrs[tests]", "coverage-enable-subprocess", "coverage[toml] (>=5.3)"] +dev = ["attrs[docs,tests]"] +docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope.interface"] +tests = ["attrs[tests-no-zope]", "zope.interface"] +tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=0.971,<0.990)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +tests_no_zope = ["cloudpickle", "hypothesis", "mypy (>=0.971,<0.990)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] [[package]] name = "bech32" @@ -48,7 +50,7 @@ python-versions = "*" [[package]] name = "black" -version = "22.10.0" +version = "22.12.0" description = "The uncompromising code formatter." category = "dev" optional = false @@ -71,7 +73,7 @@ uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "certifi" -version = "2022.9.24" +version = "2022.12.7" description = "Python package for providing Mozilla's CA Bundle." category = "main" optional = false @@ -113,15 +115,15 @@ importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} [[package]] name = "colorama" -version = "0.4.5" +version = "0.4.6" description = "Cross-platform colored terminal text." category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" [[package]] name = "coverage" -version = "6.5.0" +version = "7.0.1" description = "Code coverage measurement for Python" category = "dev" optional = false @@ -185,6 +187,17 @@ django = ["dj-database-url", "dj-email-url", "django-cache-url"] lint = ["flake8 (==4.0.1)", "flake8-bugbear (==21.9.2)", "mypy (==0.910)", "pre-commit (>=2.4,<3.0)"] tests = ["dj-database-url", "dj-email-url", "django-cache-url", "pytest"] +[[package]] +name = "exceptiongroup" +version = "1.1.0" +description = "Backport of PEP 654 (exception groups)" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.extras] +test = ["pytest (>=6)"] + [[package]] name = "fastapi" version = "0.83.0" @@ -221,7 +234,7 @@ python-versions = ">=3.5" [[package]] name = "importlib-metadata" -version = "5.0.0" +version = "5.2.0" description = "Read metadata from Python packages" category = "main" optional = false @@ -232,7 +245,7 @@ typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} zipp = ">=0.5" [package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] perf = ["ipython"] testing = ["flake8 (<5)", "flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"] @@ -246,17 +259,17 @@ python-versions = "*" [[package]] name = "isort" -version = "5.10.1" +version = "5.11.4" description = "A Python utility / library to sort Python imports." category = "dev" optional = false -python-versions = ">=3.6.1,<4.0" +python-versions = ">=3.7.0" [package.extras] colors = ["colorama (>=0.4.3,<0.5.0)"] -pipfile_deprecated_finder = ["pipreqs", "requirementslib"] +pipfile-deprecated-finder = ["pipreqs", "requirementslib"] plugins = ["setuptools"] -requirements_deprecated_finder = ["pip-api", "pipreqs"] +requirements-deprecated-finder = ["pip-api", "pipreqs"] [[package]] name = "loguru" @@ -275,7 +288,7 @@ dev = ["Sphinx (>=4.1.1)", "black (>=19.10b0)", "colorama (>=0.3.4)", "docutils [[package]] name = "marshmallow" -version = "3.18.0" +version = "3.19.0" description = "A lightweight library for converting complex datatypes to and from native Python datatypes." category = "main" optional = false @@ -285,9 +298,9 @@ python-versions = ">=3.7" packaging = ">=17.0" [package.extras] -dev = ["flake8 (==5.0.4)", "flake8-bugbear (==22.9.11)", "mypy (==0.971)", "pre-commit (>=2.4,<3.0)", "pytest", "pytz", "simplejson", "tox"] -docs = ["alabaster (==0.7.12)", "autodocsumm (==0.2.9)", "sphinx (==5.1.1)", "sphinx-issues (==3.0.1)", "sphinx-version-warning (==1.1.2)"] -lint = ["flake8 (==5.0.4)", "flake8-bugbear (==22.9.11)", "mypy (==0.971)", "pre-commit (>=2.4,<3.0)"] +dev = ["flake8 (==5.0.4)", "flake8-bugbear (==22.10.25)", "mypy (==0.990)", "pre-commit (>=2.4,<3.0)", "pytest", "pytz", "simplejson", "tox"] +docs = ["alabaster (==0.7.12)", "autodocsumm (==0.2.9)", "sphinx (==5.3.0)", "sphinx-issues (==3.0.1)", "sphinx-version-warning (==1.1.2)"] +lint = ["flake8 (==5.0.4)", "flake8-bugbear (==22.10.25)", "mypy (==0.990)", "pre-commit (>=2.4,<3.0)"] tests = ["pytest", "pytz", "simplejson"] [[package]] @@ -330,18 +343,15 @@ attrs = ">=19.2.0" [[package]] name = "packaging" -version = "21.3" +version = "22.0" description = "Core utilities for Python packages" category = "main" optional = false -python-versions = ">=3.6" - -[package.dependencies] -pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" +python-versions = ">=3.7" [[package]] name = "pathspec" -version = "0.10.1" +version = "0.10.3" description = "Utility library for gitignore style pattern matching of file paths." category = "dev" optional = false @@ -349,15 +359,15 @@ python-versions = ">=3.7" [[package]] name = "platformdirs" -version = "2.5.2" -description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +version = "2.6.0" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." category = "dev" optional = false python-versions = ">=3.7" [package.extras] -docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx (>=4)", "sphinx-autodoc-typehints (>=1.12)"] -test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] +docs = ["furo (>=2022.9.29)", "proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.4)"] +test = ["appdirs (==1.4.4)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] [[package]] name = "pluggy" @@ -374,14 +384,6 @@ importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] -[[package]] -name = "py" -version = "1.11.0" -description = "library with cross-python path, ini-parsing, io, code, log facilities" -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" - [[package]] name = "pycparser" version = "2.21" @@ -413,17 +415,6 @@ typing-extensions = ">=4.1.0" dotenv = ["python-dotenv (>=0.10.4)"] email = ["email-validator (>=1.0.3)"] -[[package]] -name = "pyparsing" -version = "3.0.9" -description = "pyparsing module - Classes and methods to define and execute parsing grammars" -category = "main" -optional = false -python-versions = ">=3.6.8" - -[package.extras] -diagrams = ["jinja2", "railroad-diagrams"] - [[package]] name = "PySocks" version = "1.7.1" @@ -434,7 +425,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "pytest" -version = "7.1.3" +version = "7.2.0" description = "pytest: simple powerful testing with Python" category = "main" optional = false @@ -443,12 +434,12 @@ python-versions = ">=3.7" [package.dependencies] attrs = ">=19.2.0" colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} iniconfig = "*" packaging = "*" pluggy = ">=0.12,<2.0" -py = ">=1.8.2" -tomli = ">=1.0.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} [package.extras] testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] @@ -640,11 +631,11 @@ python-versions = ">=3.7" [[package]] name = "urllib3" -version = "1.26.12" +version = "1.26.13" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" [package.extras] brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] @@ -693,7 +684,7 @@ dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"] [[package]] name = "zipp" -version = "3.9.0" +version = "3.11.0" description = "Backport of pathlib-compatible object wrapper for zip files" category = "main" optional = false @@ -714,8 +705,8 @@ anyio = [ {file = "anyio-3.6.2.tar.gz", hash = "sha256:25ea0d673ae30af41a0c442f81cf3b38c7e79fdc7b60335a4c14e05eb0947421"}, ] attrs = [ - {file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"}, - {file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"}, + {file = "attrs-22.2.0-py3-none-any.whl", hash = "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836"}, + {file = "attrs-22.2.0.tar.gz", hash = "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99"}, ] bech32 = [ {file = "bech32-1.2.0-py3-none-any.whl", hash = "sha256:990dc8e5a5e4feabbdf55207b5315fdd9b73db40be294a19b3752cde9e79d981"}, @@ -727,31 +718,22 @@ bitstring = [ {file = "bitstring-3.1.9.tar.gz", hash = "sha256:a5848a3f63111785224dca8bb4c0a75b62ecdef56a042c8d6be74b16f7e860e7"}, ] black = [ - {file = "black-22.10.0-1fixedarch-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:5cc42ca67989e9c3cf859e84c2bf014f6633db63d1cbdf8fdb666dcd9e77e3fa"}, - {file = "black-22.10.0-1fixedarch-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:5d8f74030e67087b219b032aa33a919fae8806d49c867846bfacde57f43972ef"}, - {file = "black-22.10.0-1fixedarch-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:197df8509263b0b8614e1df1756b1dd41be6738eed2ba9e9769f3880c2b9d7b6"}, - {file = "black-22.10.0-1fixedarch-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:2644b5d63633702bc2c5f3754b1b475378fbbfb481f62319388235d0cd104c2d"}, - {file = "black-22.10.0-1fixedarch-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:e41a86c6c650bcecc6633ee3180d80a025db041a8e2398dcc059b3afa8382cd4"}, - {file = "black-22.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2039230db3c6c639bd84efe3292ec7b06e9214a2992cd9beb293d639c6402edb"}, - {file = "black-22.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14ff67aec0a47c424bc99b71005202045dc09270da44a27848d534600ac64fc7"}, - {file = "black-22.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:819dc789f4498ecc91438a7de64427c73b45035e2e3680c92e18795a839ebb66"}, - {file = "black-22.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5b9b29da4f564ba8787c119f37d174f2b69cdfdf9015b7d8c5c16121ddc054ae"}, - {file = "black-22.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8b49776299fece66bffaafe357d929ca9451450f5466e997a7285ab0fe28e3b"}, - {file = "black-22.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:21199526696b8f09c3997e2b4db8d0b108d801a348414264d2eb8eb2532e540d"}, - {file = "black-22.10.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e464456d24e23d11fced2bc8c47ef66d471f845c7b7a42f3bd77bf3d1789650"}, - {file = "black-22.10.0-cp37-cp37m-win_amd64.whl", hash = "sha256:9311e99228ae10023300ecac05be5a296f60d2fd10fff31cf5c1fa4ca4b1988d"}, - {file = "black-22.10.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:fba8a281e570adafb79f7755ac8721b6cf1bbf691186a287e990c7929c7692ff"}, - {file = "black-22.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:915ace4ff03fdfff953962fa672d44be269deb2eaf88499a0f8805221bc68c87"}, - {file = "black-22.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:444ebfb4e441254e87bad00c661fe32df9969b2bf224373a448d8aca2132b395"}, - {file = "black-22.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:974308c58d057a651d182208a484ce80a26dac0caef2895836a92dd6ebd725e0"}, - {file = "black-22.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72ef3925f30e12a184889aac03d77d031056860ccae8a1e519f6cbb742736383"}, - {file = "black-22.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:432247333090c8c5366e69627ccb363bc58514ae3e63f7fc75c54b1ea80fa7de"}, - {file = "black-22.10.0-py3-none-any.whl", hash = "sha256:c957b2b4ea88587b46cf49d1dc17681c1e672864fd7af32fc1e9664d572b3458"}, - {file = "black-22.10.0.tar.gz", hash = "sha256:f513588da599943e0cde4e32cc9879e825d58720d6557062d1098c5ad80080e1"}, + {file = "black-22.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9eedd20838bd5d75b80c9f5487dbcb06836a43833a37846cf1d8c1cc01cef59d"}, + {file = "black-22.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:159a46a4947f73387b4d83e87ea006dbb2337eab6c879620a3ba52699b1f4351"}, + {file = "black-22.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d30b212bffeb1e252b31dd269dfae69dd17e06d92b87ad26e23890f3efea366f"}, + {file = "black-22.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:7412e75863aa5c5411886804678b7d083c7c28421210180d67dfd8cf1221e1f4"}, + {file = "black-22.12.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c116eed0efb9ff870ded8b62fe9f28dd61ef6e9ddd28d83d7d264a38417dcee2"}, + {file = "black-22.12.0-cp37-cp37m-win_amd64.whl", hash = "sha256:1f58cbe16dfe8c12b7434e50ff889fa479072096d79f0a7f25e4ab8e94cd8350"}, + {file = "black-22.12.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77d86c9f3db9b1bf6761244bc0b3572a546f5fe37917a044e02f3166d5aafa7d"}, + {file = "black-22.12.0-cp38-cp38-win_amd64.whl", hash = "sha256:82d9fe8fee3401e02e79767016b4907820a7dc28d70d137eb397b92ef3cc5bfc"}, + {file = "black-22.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:101c69b23df9b44247bd88e1d7e90154336ac4992502d4197bdac35dd7ee3320"}, + {file = "black-22.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:559c7a1ba9a006226f09e4916060982fd27334ae1998e7a38b3f33a37f7a2148"}, + {file = "black-22.12.0-py3-none-any.whl", hash = "sha256:436cc9167dd28040ad90d3b404aec22cedf24a6e4d7de221bec2730ec0c97bcf"}, + {file = "black-22.12.0.tar.gz", hash = "sha256:229351e5a18ca30f447bf724d007f890f97e13af070bb6ad4c0a441cd7596a2f"}, ] certifi = [ - {file = "certifi-2022.9.24-py3-none-any.whl", hash = "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382"}, - {file = "certifi-2022.9.24.tar.gz", hash = "sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14"}, + {file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"}, + {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"}, ] cffi = [ {file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"}, @@ -828,60 +810,61 @@ click = [ {file = "click-8.0.4.tar.gz", hash = "sha256:8458d7b1287c5fb128c90e23381cf99dcde74beaf6c7ff6384ce84d6fe090adb"}, ] colorama = [ - {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"}, - {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] coverage = [ - {file = "coverage-6.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef8674b0ee8cc11e2d574e3e2998aea5df5ab242e012286824ea3c6970580e53"}, - {file = "coverage-6.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:784f53ebc9f3fd0e2a3f6a78b2be1bd1f5575d7863e10c6e12504f240fd06660"}, - {file = "coverage-6.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4a5be1748d538a710f87542f22c2cad22f80545a847ad91ce45e77417293eb4"}, - {file = "coverage-6.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83516205e254a0cb77d2d7bb3632ee019d93d9f4005de31dca0a8c3667d5bc04"}, - {file = "coverage-6.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af4fffaffc4067232253715065e30c5a7ec6faac36f8fc8d6f64263b15f74db0"}, - {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:97117225cdd992a9c2a5515db1f66b59db634f59d0679ca1fa3fe8da32749cae"}, - {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a1170fa54185845505fbfa672f1c1ab175446c887cce8212c44149581cf2d466"}, - {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:11b990d520ea75e7ee8dcab5bc908072aaada194a794db9f6d7d5cfd19661e5a"}, - {file = "coverage-6.5.0-cp310-cp310-win32.whl", hash = "sha256:5dbec3b9095749390c09ab7c89d314727f18800060d8d24e87f01fb9cfb40b32"}, - {file = "coverage-6.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:59f53f1dc5b656cafb1badd0feb428c1e7bc19b867479ff72f7a9dd9b479f10e"}, - {file = "coverage-6.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4a5375e28c5191ac38cca59b38edd33ef4cc914732c916f2929029b4bfb50795"}, - {file = "coverage-6.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4ed2820d919351f4167e52425e096af41bfabacb1857186c1ea32ff9983ed75"}, - {file = "coverage-6.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:33a7da4376d5977fbf0a8ed91c4dffaaa8dbf0ddbf4c8eea500a2486d8bc4d7b"}, - {file = "coverage-6.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8fb6cf131ac4070c9c5a3e21de0f7dc5a0fbe8bc77c9456ced896c12fcdad91"}, - {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a6b7d95969b8845250586f269e81e5dfdd8ff828ddeb8567a4a2eaa7313460c4"}, - {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1ef221513e6f68b69ee9e159506d583d31aa3567e0ae84eaad9d6ec1107dddaa"}, - {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cca4435eebea7962a52bdb216dec27215d0df64cf27fc1dd538415f5d2b9da6b"}, - {file = "coverage-6.5.0-cp311-cp311-win32.whl", hash = "sha256:98e8a10b7a314f454d9eff4216a9a94d143a7ee65018dd12442e898ee2310578"}, - {file = "coverage-6.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:bc8ef5e043a2af066fa8cbfc6e708d58017024dc4345a1f9757b329a249f041b"}, - {file = "coverage-6.5.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4433b90fae13f86fafff0b326453dd42fc9a639a0d9e4eec4d366436d1a41b6d"}, - {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4f05d88d9a80ad3cac6244d36dd89a3c00abc16371769f1340101d3cb899fc3"}, - {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:94e2565443291bd778421856bc975d351738963071e9b8839ca1fc08b42d4bef"}, - {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:027018943386e7b942fa832372ebc120155fd970837489896099f5cfa2890f79"}, - {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:255758a1e3b61db372ec2736c8e2a1fdfaf563977eedbdf131de003ca5779b7d"}, - {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:851cf4ff24062c6aec510a454b2584f6e998cada52d4cb58c5e233d07172e50c"}, - {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:12adf310e4aafddc58afdb04d686795f33f4d7a6fa67a7a9d4ce7d6ae24d949f"}, - {file = "coverage-6.5.0-cp37-cp37m-win32.whl", hash = "sha256:b5604380f3415ba69de87a289a2b56687faa4fe04dbee0754bfcae433489316b"}, - {file = "coverage-6.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:4a8dbc1f0fbb2ae3de73eb0bdbb914180c7abfbf258e90b311dcd4f585d44bd2"}, - {file = "coverage-6.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d900bb429fdfd7f511f868cedd03a6bbb142f3f9118c09b99ef8dc9bf9643c3c"}, - {file = "coverage-6.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2198ea6fc548de52adc826f62cb18554caedfb1d26548c1b7c88d8f7faa8f6ba"}, - {file = "coverage-6.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c4459b3de97b75e3bd6b7d4b7f0db13f17f504f3d13e2a7c623786289dd670e"}, - {file = "coverage-6.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:20c8ac5386253717e5ccc827caad43ed66fea0efe255727b1053a8154d952398"}, - {file = "coverage-6.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b07130585d54fe8dff3d97b93b0e20290de974dc8177c320aeaf23459219c0b"}, - {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:dbdb91cd8c048c2b09eb17713b0c12a54fbd587d79adcebad543bc0cd9a3410b"}, - {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:de3001a203182842a4630e7b8d1a2c7c07ec1b45d3084a83d5d227a3806f530f"}, - {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e07f4a4a9b41583d6eabec04f8b68076ab3cd44c20bd29332c6572dda36f372e"}, - {file = "coverage-6.5.0-cp38-cp38-win32.whl", hash = "sha256:6d4817234349a80dbf03640cec6109cd90cba068330703fa65ddf56b60223a6d"}, - {file = "coverage-6.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:7ccf362abd726b0410bf8911c31fbf97f09f8f1061f8c1cf03dfc4b6372848f6"}, - {file = "coverage-6.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:633713d70ad6bfc49b34ead4060531658dc6dfc9b3eb7d8a716d5873377ab745"}, - {file = "coverage-6.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:95203854f974e07af96358c0b261f1048d8e1083f2de9b1c565e1be4a3a48cfc"}, - {file = "coverage-6.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9023e237f4c02ff739581ef35969c3739445fb059b060ca51771e69101efffe"}, - {file = "coverage-6.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:265de0fa6778d07de30bcf4d9dc471c3dc4314a23a3c6603d356a3c9abc2dfcf"}, - {file = "coverage-6.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f830ed581b45b82451a40faabb89c84e1a998124ee4212d440e9c6cf70083e5"}, - {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7b6be138d61e458e18d8e6ddcddd36dd96215edfe5f1168de0b1b32635839b62"}, - {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:42eafe6778551cf006a7c43153af1211c3aaab658d4d66fa5fcc021613d02518"}, - {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:723e8130d4ecc8f56e9a611e73b31219595baa3bb252d539206f7bbbab6ffc1f"}, - {file = "coverage-6.5.0-cp39-cp39-win32.whl", hash = "sha256:d9ecf0829c6a62b9b573c7bb6d4dcd6ba8b6f80be9ba4fc7ed50bf4ac9aecd72"}, - {file = "coverage-6.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:fc2af30ed0d5ae0b1abdb4ebdce598eafd5b35397d4d75deb341a614d333d987"}, - {file = "coverage-6.5.0-pp36.pp37.pp38-none-any.whl", hash = "sha256:1431986dac3923c5945271f169f59c45b8802a114c8f548d611f2015133df77a"}, - {file = "coverage-6.5.0.tar.gz", hash = "sha256:f642e90754ee3e06b0e7e51bce3379590e76b7f76b708e1a71ff043f87025c84"}, + {file = "coverage-7.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b3695c4f4750bca943b3e1f74ad4be8d29e4aeab927d50772c41359107bd5d5c"}, + {file = "coverage-7.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fa6a5a224b7f4cfb226f4fc55a57e8537fcc096f42219128c2c74c0e7d0953e1"}, + {file = "coverage-7.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74f70cd92669394eaf8d7756d1b195c8032cf7bbbdfce3bc489d4e15b3b8cf73"}, + {file = "coverage-7.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b66bb21a23680dee0be66557dc6b02a3152ddb55edf9f6723fa4a93368f7158d"}, + {file = "coverage-7.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d87717959d4d0ee9db08a0f1d80d21eb585aafe30f9b0a54ecf779a69cb015f6"}, + {file = "coverage-7.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:854f22fa361d1ff914c7efa347398374cc7d567bdafa48ac3aa22334650dfba2"}, + {file = "coverage-7.0.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:1e414dc32ee5c3f36544ea466b6f52f28a7af788653744b8570d0bf12ff34bc0"}, + {file = "coverage-7.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6c5ad996c6fa4d8ed669cfa1e8551348729d008a2caf81489ab9ea67cfbc7498"}, + {file = "coverage-7.0.1-cp310-cp310-win32.whl", hash = "sha256:691571f31ace1837838b7e421d3a09a8c00b4aac32efacb4fc9bd0a5c647d25a"}, + {file = "coverage-7.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:89caf4425fe88889e2973a8e9a3f6f5f9bbe5dd411d7d521e86428c08a873a4a"}, + {file = "coverage-7.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:63d56165a7c76265468d7e0c5548215a5ba515fc2cba5232d17df97bffa10f6c"}, + {file = "coverage-7.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4f943a3b2bc520102dd3e0bb465e1286e12c9a54f58accd71b9e65324d9c7c01"}, + {file = "coverage-7.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:830525361249dc4cd013652b0efad645a385707a5ae49350c894b67d23fbb07c"}, + {file = "coverage-7.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd1b9c5adc066db699ccf7fa839189a649afcdd9e02cb5dc9d24e67e7922737d"}, + {file = "coverage-7.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e00c14720b8b3b6c23b487e70bd406abafc976ddc50490f645166f111c419c39"}, + {file = "coverage-7.0.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6d55d840e1b8c0002fce66443e124e8581f30f9ead2e54fbf6709fb593181f2c"}, + {file = "coverage-7.0.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:66b18c3cf8bbab0cce0d7b9e4262dc830e93588986865a8c78ab2ae324b3ed56"}, + {file = "coverage-7.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:12a5aa77783d49e05439fbe6e6b427484f8a0f9f456b46a51d8aac022cfd024d"}, + {file = "coverage-7.0.1-cp311-cp311-win32.whl", hash = "sha256:b77015d1cb8fe941be1222a5a8b4e3fbca88180cfa7e2d4a4e58aeabadef0ab7"}, + {file = "coverage-7.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:fb992c47cb1e5bd6a01e97182400bcc2ba2077080a17fcd7be23aaa6e572e390"}, + {file = "coverage-7.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e78e9dcbf4f3853d3ae18a8f9272111242531535ec9e1009fa8ec4a2b74557dc"}, + {file = "coverage-7.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e60bef2e2416f15fdc05772bf87db06c6a6f9870d1db08fdd019fbec98ae24a9"}, + {file = "coverage-7.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9823e4789ab70f3ec88724bba1a203f2856331986cd893dedbe3e23a6cfc1e4e"}, + {file = "coverage-7.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9158f8fb06747ac17bd237930c4372336edc85b6e13bdc778e60f9d685c3ca37"}, + {file = "coverage-7.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:486ee81fa694b4b796fc5617e376326a088f7b9729c74d9defa211813f3861e4"}, + {file = "coverage-7.0.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:1285648428a6101b5f41a18991c84f1c3959cee359e51b8375c5882fc364a13f"}, + {file = "coverage-7.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2c44fcfb3781b41409d0f060a4ed748537557de9362a8a9282182fafb7a76ab4"}, + {file = "coverage-7.0.1-cp37-cp37m-win32.whl", hash = "sha256:d6814854c02cbcd9c873c0f3286a02e3ac1250625cca822ca6bc1018c5b19f1c"}, + {file = "coverage-7.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:f66460f17c9319ea4f91c165d46840314f0a7c004720b20be58594d162a441d8"}, + {file = "coverage-7.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9b373c9345c584bb4b5f5b8840df7f4ab48c4cbb7934b58d52c57020d911b856"}, + {file = "coverage-7.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d3022c3007d3267a880b5adcf18c2a9bf1fc64469b394a804886b401959b8742"}, + {file = "coverage-7.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92651580bd46519067e36493acb394ea0607b55b45bd81dd4e26379ed1871f55"}, + {file = "coverage-7.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3cfc595d2af13856505631be072835c59f1acf30028d1c860b435c5fc9c15b69"}, + {file = "coverage-7.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b4b3a4d9915b2be879aff6299c0a6129f3d08a775d5a061f503cf79571f73e4"}, + {file = "coverage-7.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b6f22bb64cc39bcb883e5910f99a27b200fdc14cdd79df8696fa96b0005c9444"}, + {file = "coverage-7.0.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:72d1507f152abacea81f65fee38e4ef3ac3c02ff8bc16f21d935fd3a8a4ad910"}, + {file = "coverage-7.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0a79137fc99815fff6a852c233628e735ec15903cfd16da0f229d9c4d45926ab"}, + {file = "coverage-7.0.1-cp38-cp38-win32.whl", hash = "sha256:b3763e7fcade2ff6c8e62340af9277f54336920489ceb6a8cd6cc96da52fcc62"}, + {file = "coverage-7.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:09f6b5a8415b6b3e136d5fec62b552972187265cb705097bf030eb9d4ffb9b60"}, + {file = "coverage-7.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:978258fec36c154b5e250d356c59af7d4c3ba02bef4b99cda90b6029441d797d"}, + {file = "coverage-7.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:19ec666533f0f70a0993f88b8273057b96c07b9d26457b41863ccd021a043b9a"}, + {file = "coverage-7.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cfded268092a84605f1cc19e5c737f9ce630a8900a3589e9289622db161967e9"}, + {file = "coverage-7.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07bcfb1d8ac94af886b54e18a88b393f6a73d5959bb31e46644a02453c36e475"}, + {file = "coverage-7.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:397b4a923cc7566bbc7ae2dfd0ba5a039b61d19c740f1373791f2ebd11caea59"}, + {file = "coverage-7.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:aec2d1515d9d39ff270059fd3afbb3b44e6ec5758af73caf18991807138c7118"}, + {file = "coverage-7.0.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c20cfebcc149a4c212f6491a5f9ff56f41829cd4f607b5be71bb2d530ef243b1"}, + {file = "coverage-7.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:fd556ff16a57a070ce4f31c635953cc44e25244f91a0378c6e9bdfd40fdb249f"}, + {file = "coverage-7.0.1-cp39-cp39-win32.whl", hash = "sha256:b9ea158775c7c2d3e54530a92da79496fb3fb577c876eec761c23e028f1e216c"}, + {file = "coverage-7.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:d1991f1dd95eba69d2cd7708ff6c2bbd2426160ffc73c2b81f617a053ebcb1a8"}, + {file = "coverage-7.0.1-pp37.pp38.pp39-none-any.whl", hash = "sha256:3dd4ee135e08037f458425b8842d24a95a0961831a33f89685ff86b77d378f89"}, + {file = "coverage-7.0.1.tar.gz", hash = "sha256:a4a574a19eeb67575a5328a5760bbbb737faa685616586a9f9da4281f940109c"}, ] cryptography = [ {file = "cryptography-38.0.4-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:2fa36a7b2cc0998a3a4d5af26ccb6273f3df133d61da2ba13b3286261e7efb70"}, @@ -919,6 +902,10 @@ environs = [ {file = "environs-9.5.0-py2.py3-none-any.whl", hash = "sha256:1e549569a3de49c05f856f40bce86979e7d5ffbbc4398e7f338574c220189124"}, {file = "environs-9.5.0.tar.gz", hash = "sha256:a76307b36fbe856bdca7ee9161e6c466fd7fcffc297109a118c59b54e27e30c9"}, ] +exceptiongroup = [ + {file = "exceptiongroup-1.1.0-py3-none-any.whl", hash = "sha256:327cbda3da756e2de031a3107b81ab7b3770a602c4d16ca618298c526f4bec1e"}, + {file = "exceptiongroup-1.1.0.tar.gz", hash = "sha256:bcb67d800a4497e1b404c2dd44fca47d3b7a5e5433dbab67f96c1a685cdfdf23"}, +] fastapi = [ {file = "fastapi-0.83.0-py3-none-any.whl", hash = "sha256:694a2b6c2607a61029a4be1c6613f84d74019cb9f7a41c7a475dca8e715f9368"}, {file = "fastapi-0.83.0.tar.gz", hash = "sha256:96eb692350fe13d7a9843c3c87a874f0d45102975257dd224903efd6c0fde3bd"}, @@ -932,24 +919,24 @@ idna = [ {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, ] importlib-metadata = [ - {file = "importlib_metadata-5.0.0-py3-none-any.whl", hash = "sha256:ddb0e35065e8938f867ed4928d0ae5bf2a53b7773871bfe6bcc7e4fcdc7dea43"}, - {file = "importlib_metadata-5.0.0.tar.gz", hash = "sha256:da31db32b304314d044d3c12c79bd59e307889b287ad12ff387b3500835fc2ab"}, + {file = "importlib_metadata-5.2.0-py3-none-any.whl", hash = "sha256:0eafa39ba42bf225fc00e67f701d71f85aead9f878569caf13c3724f704b970f"}, + {file = "importlib_metadata-5.2.0.tar.gz", hash = "sha256:404d48d62bba0b7a77ff9d405efd91501bef2e67ff4ace0bed40a0cf28c3c7cd"}, ] iniconfig = [ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, ] isort = [ - {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"}, - {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, + {file = "isort-5.11.4-py3-none-any.whl", hash = "sha256:c033fd0edb91000a7f09527fe5c75321878f98322a77ddcc81adbd83724afb7b"}, + {file = "isort-5.11.4.tar.gz", hash = "sha256:6db30c5ded9815d813932c04c2f85a360bcdd35fed496f4d8f35495ef0a261b6"}, ] loguru = [ {file = "loguru-0.6.0-py3-none-any.whl", hash = "sha256:4e2414d534a2ab57573365b3e6d0234dfb1d84b68b7f3b948e6fb743860a77c3"}, {file = "loguru-0.6.0.tar.gz", hash = "sha256:066bd06758d0a513e9836fd9c6b5a75bfb3fd36841f4b996bc60b547a309d41c"}, ] marshmallow = [ - {file = "marshmallow-3.18.0-py3-none-any.whl", hash = "sha256:35e02a3a06899c9119b785c12a22f4cda361745d66a71ab691fd7610202ae104"}, - {file = "marshmallow-3.18.0.tar.gz", hash = "sha256:6804c16114f7fce1f5b4dadc31f4674af23317fcc7f075da21e35c1a35d781f7"}, + {file = "marshmallow-3.19.0-py3-none-any.whl", hash = "sha256:93f0958568da045b0021ec6aeb7ac37c81bfcccbb9a0e7ed8559885070b3a19b"}, + {file = "marshmallow-3.19.0.tar.gz", hash = "sha256:90032c0fd650ce94b6ec6dc8dfeb0e3ff50c144586462c389b81a07205bedb78"}, ] mypy = [ {file = "mypy-0.971-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f2899a3cbd394da157194f913a931edfd4be5f274a88041c9dc2d9cdcb1c315c"}, @@ -985,25 +972,21 @@ outcome = [ {file = "outcome-1.2.0.tar.gz", hash = "sha256:6f82bd3de45da303cf1f771ecafa1633750a358436a8bb60e06a1ceb745d2672"}, ] packaging = [ - {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, - {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, + {file = "packaging-22.0-py3-none-any.whl", hash = "sha256:957e2148ba0e1a3b282772e791ef1d8083648bc131c8ab0c1feba110ce1146c3"}, + {file = "packaging-22.0.tar.gz", hash = "sha256:2198ec20bd4c017b8f9717e00f0c8714076fc2fd93816750ab48e2c41de2cfd3"}, ] pathspec = [ - {file = "pathspec-0.10.1-py3-none-any.whl", hash = "sha256:46846318467efc4556ccfd27816e004270a9eeeeb4d062ce5e6fc7a87c573f93"}, - {file = "pathspec-0.10.1.tar.gz", hash = "sha256:7ace6161b621d31e7902eb6b5ae148d12cfd23f4a249b9ffb6b9fee12084323d"}, + {file = "pathspec-0.10.3-py3-none-any.whl", hash = "sha256:3c95343af8b756205e2aba76e843ba9520a24dd84f68c22b9f93251507509dd6"}, + {file = "pathspec-0.10.3.tar.gz", hash = "sha256:56200de4077d9d0791465aa9095a01d421861e405b5096955051deefd697d6f6"}, ] platformdirs = [ - {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"}, - {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"}, + {file = "platformdirs-2.6.0-py3-none-any.whl", hash = "sha256:1a89a12377800c81983db6be069ec068eee989748799b946cce2a6e80dcc54ca"}, + {file = "platformdirs-2.6.0.tar.gz", hash = "sha256:b46ffafa316e6b83b47489d240ce17173f123a9b9c83282141c3daf26ad9ac2e"}, ] pluggy = [ {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, ] -py = [ - {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, - {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, -] pycparser = [ {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, @@ -1074,18 +1057,14 @@ pydantic = [ {file = "pydantic-1.10.2-py3-none-any.whl", hash = "sha256:1b6ee725bd6e83ec78b1aa32c5b1fa67a3a65badddde3976bca5fe4568f27709"}, {file = "pydantic-1.10.2.tar.gz", hash = "sha256:91b8e218852ef6007c2b98cd861601c6a09f1aa32bbbb74fab5b1c33d4a1e410"}, ] -pyparsing = [ - {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, - {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, -] PySocks = [ {file = "PySocks-1.7.1-py27-none-any.whl", hash = "sha256:08e69f092cc6dbe92a0fdd16eeb9b9ffbc13cadfe5ca4c7bd92ffb078b293299"}, {file = "PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5"}, {file = "PySocks-1.7.1.tar.gz", hash = "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0"}, ] pytest = [ - {file = "pytest-7.1.3-py3-none-any.whl", hash = "sha256:1377bda3466d70b55e3f5cecfa55bb7cfcf219c7964629b967c37cf0bda818b7"}, - {file = "pytest-7.1.3.tar.gz", hash = "sha256:4f365fec2dff9c1162f834d9f18af1ba13062db0c708bf7b946f8a5c76180c39"}, + {file = "pytest-7.2.0-py3-none-any.whl", hash = "sha256:892f933d339f068883b6fd5a459f03d85bfcb355e4981e146d2c7616c21fef71"}, + {file = "pytest-7.2.0.tar.gz", hash = "sha256:c4014eb40e10f11f355ad4e3c2fb2c6c6d1919c73f3b5a433de4708202cade59"}, ] pytest-asyncio = [ {file = "pytest-asyncio-0.19.0.tar.gz", hash = "sha256:ac4ebf3b6207259750bc32f4c1d8fcd7e79739edbc67ad0c58dd150b1d072fed"}, @@ -1223,8 +1202,8 @@ typing-extensions = [ {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"}, ] urllib3 = [ - {file = "urllib3-1.26.12-py2.py3-none-any.whl", hash = "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997"}, - {file = "urllib3-1.26.12.tar.gz", hash = "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e"}, + {file = "urllib3-1.26.13-py2.py3-none-any.whl", hash = "sha256:47cc05d99aaa09c9e72ed5809b60e7ba354e64b59c9c173ac3018642d8bb41fc"}, + {file = "urllib3-1.26.13.tar.gz", hash = "sha256:c083dd0dce68dbfbe1129d5271cb90f9447dea7d52097c6e0126120c521ddea8"}, ] uvicorn = [ {file = "uvicorn-0.18.3-py3-none-any.whl", hash = "sha256:0abd429ebb41e604ed8d2be6c60530de3408f250e8d2d84967d85ba9e86fe3af"}, @@ -1239,6 +1218,6 @@ win32-setctime = [ {file = "win32_setctime-1.1.0.tar.gz", hash = "sha256:15cf5750465118d6929ae4de4eb46e8edae9a5634350c01ba582df868e932cb2"}, ] zipp = [ - {file = "zipp-3.9.0-py3-none-any.whl", hash = "sha256:972cfa31bc2fedd3fa838a51e9bc7e64b7fb725a8c00e7431554311f180e9980"}, - {file = "zipp-3.9.0.tar.gz", hash = "sha256:3a7af91c3db40ec72dd9d154ae18e008c69efe8ca88dde4f9a731bb82fe2f9eb"}, + {file = "zipp-3.11.0-py3-none-any.whl", hash = "sha256:83a28fcb75844b5c0cdaf5aa4003c2d728c77e05f5aeabe8e95e56727005fbaa"}, + {file = "zipp-3.11.0.tar.gz", hash = "sha256:a7a22e05929290a67401440b39690ae6563279bced5f314609d9d03798f56766"}, ] From 96018cee452b854fd7edd50accf61eab8e6b4058 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Mon, 26 Dec 2022 16:37:02 +0100 Subject: [PATCH 04/15] use receive function of cli --- cashu/wallet/cli.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/cashu/wallet/cli.py b/cashu/wallet/cli.py index 06a0863..8f39ba1 100644 --- a/cashu/wallet/cli.py +++ b/cashu/wallet/cli.py @@ -457,17 +457,12 @@ async def nostr(ctx): await asyncio.sleep(2) def get_token_callback(event: Event, decrypted_content): - print( - f"From {event.public_key[:3]}..{event.public_key[-3:]}: {decrypted_content}" - ) + # print( + # f"From {event.public_key[:3]}..{event.public_key[-3:]}: {decrypted_content}" + # ) try: - proofs = [ - Proof(**p) - for p in json.loads(base64.urlsafe_b64decode(decrypted_content)) - ] - wallet: Wallet = ctx.obj["WALLET"] - asyncio.run(wallet.redeem(proofs)) - wallet.status() + # call the receive method + asyncio.run(receive(ctx, decrypted_content)) except Exception as e: pass From 5a8dc1c898a983574836f001faf6b60977074b3e Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Mon, 26 Dec 2022 16:46:45 +0100 Subject: [PATCH 05/15] refactor receive --- cashu/wallet/cli.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/cashu/wallet/cli.py b/cashu/wallet/cli.py index 8f39ba1..93d8c2b 100644 --- a/cashu/wallet/cli.py +++ b/cashu/wallet/cli.py @@ -233,11 +233,6 @@ async def send(ctx, amount: int, lock: str): wallet.status() -@cli.command("receive", help="Receive tokens.") -@click.argument("token", type=str) -@click.option("--lock", "-l", default=None, help="Unlock tokens.", type=str) -@click.pass_context -@coro async def receive(ctx, token: str, lock: str): wallet: Wallet = ctx.obj["WALLET"] await wallet.load_mint() @@ -260,6 +255,15 @@ async def receive(ctx, token: str, lock: str): wallet.status() +@cli.command("receive", help="Receive tokens.") +@click.argument("token", type=str) +@click.option("--lock", "-l", default=None, help="Unlock tokens.", type=str) +@click.pass_context +@coro +async def receive_cli(ctx, token: str, lock: str): + await receive(ctx, token, lock) + + @cli.command("burn", help="Burn spent tokens.") @click.argument("token", required=False, type=str) @click.option("--all", "-a", default=False, is_flag=True, help="Burn all spent tokens.") @@ -455,14 +459,15 @@ async def nostr(ctx): client = NostrClient(privatekey_hex=NOSTR_PRIVATE_KEY) await asyncio.sleep(2) + await receive(ctx, "asd", "") def get_token_callback(event: Event, decrypted_content): - # print( - # f"From {event.public_key[:3]}..{event.public_key[-3:]}: {decrypted_content}" - # ) + print( + f"From {event.public_key[:3]}..{event.public_key[-3:]}: {decrypted_content}" + ) try: # call the receive method - asyncio.run(receive(ctx, decrypted_content)) + asyncio.run(receive(ctx, decrypted_content, "")) except Exception as e: pass From 74529f9b41e9eae104783bb13f98cb9459281c7e Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Mon, 26 Dec 2022 16:50:28 +0100 Subject: [PATCH 06/15] clean --- cashu/wallet/cli.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cashu/wallet/cli.py b/cashu/wallet/cli.py index 93d8c2b..5744c66 100644 --- a/cashu/wallet/cli.py +++ b/cashu/wallet/cli.py @@ -459,7 +459,6 @@ async def nostr(ctx): client = NostrClient(privatekey_hex=NOSTR_PRIVATE_KEY) await asyncio.sleep(2) - await receive(ctx, "asd", "") def get_token_callback(event: Event, decrypted_content): print( From c311ef2e8bba133b2f96e54ba161c87756a37e75 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Mon, 26 Dec 2022 16:59:43 +0100 Subject: [PATCH 07/15] clean output --- cashu/nostr | 2 +- cashu/wallet/cli.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cashu/nostr b/cashu/nostr index dfca8bf..13c8a8d 160000 --- a/cashu/nostr +++ b/cashu/nostr @@ -1 +1 @@ -Subproject commit dfca8bfdc59732fca4c04d3ce762dedb00b2598f +Subproject commit 13c8a8d1d55daf88efb64e84c82db8961c0c9a97 diff --git a/cashu/wallet/cli.py b/cashu/wallet/cli.py index 5744c66..55af5a0 100644 --- a/cashu/wallet/cli.py +++ b/cashu/wallet/cli.py @@ -236,7 +236,6 @@ async def send(ctx, amount: int, lock: str): async def receive(ctx, token: str, lock: str): wallet: Wallet = ctx.obj["WALLET"] await wallet.load_mint() - wallet.status() if lock: # load the script and signature of this address from the database assert len(lock.split("P2SH:")) == 2, Exception( @@ -261,6 +260,8 @@ async def receive(ctx, token: str, lock: str): @click.pass_context @coro async def receive_cli(ctx, token: str, lock: str): + wallet: Wallet = ctx.obj["WALLET"] + wallet.status() await receive(ctx, token, lock) @@ -450,7 +451,6 @@ async def info(ctx): @coro async def nostr(ctx): wallet: Wallet = ctx.obj["WALLET"] - await wallet.load_mint() if NOSTR_PRIVATE_KEY is None: print( "Warning!\n\nYou don't have a private key set in your .env file. I will create a random private key for this session but I will not remember it. If you lose this key, you will lose access to the DMs you receive on it." @@ -461,9 +461,9 @@ async def nostr(ctx): await asyncio.sleep(2) def get_token_callback(event: Event, decrypted_content): - print( - f"From {event.public_key[:3]}..{event.public_key[-3:]}: {decrypted_content}" - ) + # print( + # f"From {event.public_key[:3]}..{event.public_key[-3:]}: {decrypted_content}" + # ) try: # call the receive method asyncio.run(receive(ctx, decrypted_content, "")) From f4b092f192afa6cf389b3592f19cfb08bab492ec Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Mon, 26 Dec 2022 17:19:24 +0100 Subject: [PATCH 08/15] clean --- cashu/nostr | 2 +- cashu/wallet/cli.py | 140 ++++++++++++++++++++++---------------------- mypy.ini | 2 + 3 files changed, 74 insertions(+), 70 deletions(-) create mode 100644 mypy.ini diff --git a/cashu/nostr b/cashu/nostr index 13c8a8d..d7fb45f 160000 --- a/cashu/nostr +++ b/cashu/nostr @@ -1 +1 @@ -Subproject commit 13c8a8d1d55daf88efb64e84c82db8961c0c9a97 +Subproject commit d7fb45f6a1c685be0037afd4e7aa172e0c7e3517 diff --git a/cashu/wallet/cli.py b/cashu/wallet/cli.py index 55af5a0..5efcd1c 100644 --- a/cashu/wallet/cli.py +++ b/cashu/wallet/cli.py @@ -5,6 +5,7 @@ import base64 import json import os import sys +import threading import time from datetime import datetime from functools import wraps @@ -15,7 +16,6 @@ from os.path import isdir, join import click from loguru import logger -import threading from cashu.core.base import Proof from cashu.core.helpers import sum_proofs @@ -26,12 +26,15 @@ from cashu.core.settings import ( ENV_FILE, LIGHTNING, MINT_URL, + NOSTR_PRIVATE_KEY, SOCKS_HOST, SOCKS_PORT, TOR, - NOSTR_PRIVATE_KEY, VERSION, ) +from cashu.nostr.nostr.client.client import NostrClient +from cashu.nostr.nostr.event import Event +from cashu.nostr.nostr.key import PublicKey from cashu.tor.tor import TorProxy from cashu.wallet import migrations from cashu.wallet.crud import ( @@ -41,10 +44,6 @@ from cashu.wallet.crud import ( ) from cashu.wallet.wallet import Wallet as Wallet -from cashu.nostr.nostr.client.client import NostrClient -from cashu.nostr.nostr.key import PublicKey -from cashu.nostr.nostr.event import Event - async def init_wallet(wallet: Wallet): """Performs migrations and loads proofs from db.""" @@ -402,6 +401,69 @@ async def invoices(ctx): print("No invoices found.") +@cli.command("nsend", help="Send tokens via nostr.") +@click.argument("amount", type=int) +@click.argument( + "pubkey", + type=str, +) +@click.pass_context +@coro +async def nostrsend(ctx, amount: int, pubkey: str): + wallet: Wallet = ctx.obj["WALLET"] + await wallet.load_mint() + wallet.status() + _, send_proofs = await wallet.split_to_send( + wallet.proofs, amount, set_reserved=True + ) + token = await wallet.serialize_proofs(send_proofs) + + print(token) + wallet.status() + + # we only use ephemeral private keys for sending + client = NostrClient() + await asyncio.sleep(1) + client.dm(token, PublicKey(bytes.fromhex(pubkey))) + print(f"Token sent to {pubkey}") + client.close() + + +@cli.command("nreceive", help="Receive tokens via nostr.") +@click.pass_context +@coro +async def nostr(ctx): + wallet: Wallet = ctx.obj["WALLET"] + if NOSTR_PRIVATE_KEY is None: + print( + "Warning!\n\nYou don't have NOSTR_PRIVATE_KEY set in your .env file. I will create a random private key for this session but I will not remember it. If you lose this key, you will lose access to the DMs you receive on it." + ) + print("") + client = NostrClient(privatekey_hex=NOSTR_PRIVATE_KEY) + print(f"Your nostr public key: {client.public_key.hex()}") + await asyncio.sleep(2) + + def get_token_callback(event: Event, decrypted_content): + # print( + # f"From {event.public_key[:3]}..{event.public_key[-3:]}: {decrypted_content}" + # ) + try: + # call the receive method + asyncio.run(receive(ctx, decrypted_content, "")) + except Exception as e: + pass + + t = threading.Thread( + target=client.get_dm, + args=( + client.public_key, + get_token_callback, + ), + name="Nostr DM", + ) + t.start() + + @cli.command("wallets", help="List of all available wallets.") @click.pass_context @coro @@ -440,70 +502,10 @@ async def info(ctx): print(f"Settings: {ENV_FILE}") if TOR: print(f"Tor enabled: {TOR}") + if NOSTR_PRIVATE_KEY: + client = NostrClient(privatekey_hex=NOSTR_PRIVATE_KEY, connect=False) + print(f"Nostr public key: {client.public_key.hex()}") if SOCKS_HOST: print(f"Socks proxy: {SOCKS_HOST}:{SOCKS_PORT}") print(f"Mint URL: {MINT_URL}") return - - -@cli.command("nreceive", help="Receive tokens via nostr.") -@click.pass_context -@coro -async def nostr(ctx): - wallet: Wallet = ctx.obj["WALLET"] - if NOSTR_PRIVATE_KEY is None: - print( - "Warning!\n\nYou don't have a private key set in your .env file. I will create a random private key for this session but I will not remember it. If you lose this key, you will lose access to the DMs you receive on it." - ) - print("") - client = NostrClient(privatekey_hex=NOSTR_PRIVATE_KEY) - - await asyncio.sleep(2) - - def get_token_callback(event: Event, decrypted_content): - # print( - # f"From {event.public_key[:3]}..{event.public_key[-3:]}: {decrypted_content}" - # ) - try: - # call the receive method - asyncio.run(receive(ctx, decrypted_content, "")) - except Exception as e: - pass - - t = threading.Thread( - target=client.get_dm, - args=( - client.public_key, - get_token_callback, - ), - name="Nostr DM", - ) - t.start() - - -@cli.command("nsend", help="Send tokens via nostr.") -@click.argument("amount", type=int) -@click.argument( - "pubkey", - type=str, - default="13395e6d975825cb811549b4b6ba6695c7ea8f75e1f3658d6cee2bee243195c3", -) -@click.pass_context -@coro -async def nostrsend(ctx, amount: int, pubkey: str): - wallet: Wallet = ctx.obj["WALLET"] - await wallet.load_mint() - wallet.status() - _, send_proofs = await wallet.split_to_send( - wallet.proofs, amount, set_reserved=True - ) - token = await wallet.serialize_proofs(send_proofs) - - print(token) - wallet.status() - - client = NostrClient(privatekey_hex=NOSTR_PRIVATE_KEY) - await asyncio.sleep(1) - client.dm(token, PublicKey(bytes.fromhex(pubkey))) - print(f"Token sent to {pubkey}") - client.close() diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 0000000..39efb2f --- /dev/null +++ b/mypy.ini @@ -0,0 +1,2 @@ +[mypy] +exclude = cashu/nostr \ No newline at end of file From 9fc7694c6bb93892c9de89352d657ae1931ed53b Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Mon, 26 Dec 2022 17:24:16 +0100 Subject: [PATCH 09/15] bump --- README.md | 2 +- cashu/core/settings.py | 2 +- poetry.lock | 19 ++++++++++++++++++- pyproject.toml | 3 ++- requirements.txt | 27 +++++++++++++++------------ setup.py | 2 +- 6 files changed, 38 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index a1bac8f..aad816c 100644 --- a/README.md +++ b/README.md @@ -106,7 +106,7 @@ cashu info Returns: ```bash -Version: 0.6.0 +Version: 0.7.0 Debug: False Cashu dir: /home/user/.cashu Wallet: wallet diff --git a/cashu/core/settings.py b/cashu/core/settings.py index b35077a..5865f51 100644 --- a/cashu/core/settings.py +++ b/cashu/core/settings.py @@ -55,4 +55,4 @@ LNBITS_KEY = env.str("LNBITS_KEY", default=None) NOSTR_PRIVATE_KEY = env.str("NOSTR_PRIVATE_KEY", default=None) MAX_ORDER = 64 -VERSION = "0.6.0" +VERSION = "0.7.0" diff --git a/poetry.lock b/poetry.lock index f334f30..27d9a23 100644 --- a/poetry.lock +++ b/poetry.lock @@ -536,6 +536,19 @@ python-versions = "*" [package.dependencies] cffi = ">=1.3.0" +[[package]] +name = "setuptools" +version = "65.6.3" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] + [[package]] name = "six" version = "1.16.0" @@ -697,7 +710,7 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools" [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "d26c1683860705c1936769b5baade31986d00b3318092971ebaed265a138fb96" +content-hash = "a317c12d282ef30beb2d3ea515e501e78eccfa70cdddc3429ebaf7f743f959bd" [metadata.files] anyio = [ @@ -1115,6 +1128,10 @@ secp256k1 = [ {file = "secp256k1-0.14.0-pp37-pypy37_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c9e7c024ff17e9b9d7c392bb2a917da231d6cb40ab119389ff1f51dca10339a4"}, {file = "secp256k1-0.14.0.tar.gz", hash = "sha256:82c06712d69ef945220c8b53c1a0d424c2ff6a1f64aee609030df79ad8383397"}, ] +setuptools = [ + {file = "setuptools-65.6.3-py3-none-any.whl", hash = "sha256:57f6f22bde4e042978bcd50176fdb381d7c21a9efa4041202288d3737a0c6a54"}, + {file = "setuptools-65.6.3.tar.gz", hash = "sha256:a7620757bf984b58deaf32fc8a4577a9bbc0850cf92c20e1ce41c38c19e5fb75"}, +] six = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, diff --git a/pyproject.toml b/pyproject.toml index 75fe525..74f52f7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "cashu" -version = "0.6.0" +version = "0.7.0" description = "Ecash wallet and mint." authors = ["calle "] license = "MIT" @@ -27,6 +27,7 @@ PySocks = "^1.7.1" cryptography = "^38.0.4" websocket-client = "1.3.3" pycryptodomex = "^3.16.0" +setuptools = "^65.6.3" [tool.poetry.dev-dependencies] black = {version = "^22.8.0", allow-prereleases = true} diff --git a/requirements.txt b/requirements.txt index 0c35f14..81e04b2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,44 +1,47 @@ anyio==3.6.2 ; python_version >= "3.7" and python_version < "4.0" -attrs==22.1.0 ; python_version >= "3.7" and python_version < "4.0" +attrs==22.2.0 ; python_version >= "3.7" and python_version < "4.0" bech32==1.2.0 ; python_version >= "3.7" and python_version < "4.0" bitstring==3.1.9 ; python_version >= "3.7" and python_version < "4.0" -certifi==2022.9.24 ; python_version >= "3.7" and python_version < "4.0" +certifi==2022.12.7 ; python_version >= "3.7" and python_version < "4.0" cffi==1.15.1 ; python_version >= "3.7" and python_version < "4.0" charset-normalizer==2.0.12 ; python_version >= "3.7" and python_version < "4.0" click==8.0.4 ; python_version >= "3.7" and python_version < "4.0" -colorama==0.4.5 ; python_version >= "3.7" and python_version < "4.0" and platform_system == "Windows" or python_version >= "3.7" and python_version < "4.0" and sys_platform == "win32" +colorama==0.4.6 ; python_version >= "3.7" and python_version < "4.0" and platform_system == "Windows" or python_version >= "3.7" and python_version < "4.0" and sys_platform == "win32" +cryptography==38.0.4 ; python_version >= "3.7" and python_version < "4.0" ecdsa==0.18.0 ; python_version >= "3.7" and python_version < "4.0" environs==9.5.0 ; python_version >= "3.7" and python_version < "4.0" +exceptiongroup==1.1.0 ; python_version >= "3.7" and python_version < "3.11" fastapi==0.83.0 ; python_version >= "3.7" and python_version < "4.0" h11==0.12.0 ; python_version >= "3.7" and python_version < "4.0" idna==3.4 ; python_version >= "3.7" and python_version < "4.0" -importlib-metadata==5.0.0 ; python_version >= "3.7" and python_version < "3.8" +importlib-metadata==5.2.0 ; python_version >= "3.7" and python_version < "3.8" iniconfig==1.1.1 ; python_version >= "3.7" and python_version < "4.0" loguru==0.6.0 ; python_version >= "3.7" and python_version < "4.0" -marshmallow==3.18.0 ; python_version >= "3.7" and python_version < "4.0" +marshmallow==3.19.0 ; python_version >= "3.7" and python_version < "4.0" outcome==1.2.0 ; python_version >= "3.7" and python_version < "4.0" -packaging==21.3 ; python_version >= "3.7" and python_version < "4.0" +packaging==22.0 ; python_version >= "3.7" and python_version < "4.0" pluggy==1.0.0 ; python_version >= "3.7" and python_version < "4.0" -py==1.11.0 ; python_version >= "3.7" and python_version < "4.0" pycparser==2.21 ; python_version >= "3.7" and python_version < "4.0" +pycryptodomex==3.16.0 ; python_version >= "3.7" and python_version < "4.0" pydantic==1.10.2 ; python_version >= "3.7" and python_version < "4.0" -pyparsing==3.0.9 ; python_version >= "3.7" and python_version < "4.0" pysocks==1.7.1 ; python_version >= "3.7" and python_version < "4.0" pytest-asyncio==0.19.0 ; python_version >= "3.7" and python_version < "4.0" -pytest==7.1.3 ; python_version >= "3.7" and python_version < "4.0" +pytest==7.2.0 ; python_version >= "3.7" and python_version < "4.0" python-bitcoinlib==0.11.2 ; python_version >= "3.7" and python_version < "4.0" python-dotenv==0.21.0 ; python_version >= "3.7" and python_version < "4.0" represent==1.6.0.post0 ; python_version >= "3.7" and python_version < "4.0" requests==2.27.1 ; python_version >= "3.7" and python_version < "4.0" secp256k1==0.14.0 ; python_version >= "3.7" and python_version < "4.0" +setuptools==65.6.3 ; python_version >= "3.7" and python_version < "4.0" six==1.16.0 ; python_version >= "3.7" and python_version < "4.0" sniffio==1.3.0 ; python_version >= "3.7" and python_version < "4.0" sqlalchemy-aio==0.17.0 ; python_version >= "3.7" and python_version < "4.0" sqlalchemy==1.3.24 ; python_version >= "3.7" and python_version < "4.0" starlette==0.19.1 ; python_version >= "3.7" and python_version < "4.0" -tomli==2.0.1 ; python_version >= "3.7" and python_version < "4.0" +tomli==2.0.1 ; python_version >= "3.7" and python_version < "3.11" typing-extensions==4.4.0 ; python_version >= "3.7" and python_version < "4.0" -urllib3==1.26.12 ; python_version >= "3.7" and python_version < "4" +urllib3==1.26.13 ; python_version >= "3.7" and python_version < "4.0" uvicorn==0.18.3 ; python_version >= "3.7" and python_version < "4.0" +websocket-client==1.3.3 ; python_version >= "3.7" and python_version < "4.0" win32-setctime==1.1.0 ; python_version >= "3.7" and python_version < "4.0" and sys_platform == "win32" -zipp==3.9.0 ; python_version >= "3.7" and python_version < "3.8" +zipp==3.11.0 ; python_version >= "3.7" and python_version < "3.8" diff --git a/setup.py b/setup.py index 128f113..20093d0 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ entry_points = {"console_scripts": ["cashu = cashu.wallet.cli:cli"]} setuptools.setup( name="cashu", - version="0.6.0", + version="0.7.0", description="Ecash wallet and mint with Bitcoin Lightning support", long_description=long_description, long_description_content_type="text/markdown", From 58d232c441fac027b99bb3354e0723631e328413 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Mon, 26 Dec 2022 17:41:28 +0100 Subject: [PATCH 10/15] clean --- cashu/wallet/cli.py | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/cashu/wallet/cli.py b/cashu/wallet/cli.py index 5efcd1c..13f9127 100644 --- a/cashu/wallet/cli.py +++ b/cashu/wallet/cli.py @@ -407,9 +407,17 @@ async def invoices(ctx): "pubkey", type=str, ) +@click.option( + "--verbose", + "-v", + default=False, + is_flag=True, + help="Show more information.", + type=bool, +) @click.pass_context @coro -async def nostrsend(ctx, amount: int, pubkey: str): +async def nsend(ctx, amount: int, pubkey: str, verbose: bool): wallet: Wallet = ctx.obj["WALLET"] await wallet.load_mint() wallet.status() @@ -423,6 +431,8 @@ async def nostrsend(ctx, amount: int, pubkey: str): # we only use ephemeral private keys for sending client = NostrClient() + if verbose: + print(f"Your ephemeral nostr private key: {client.private_key.hex()}") await asyncio.sleep(1) client.dm(token, PublicKey(bytes.fromhex(pubkey))) print(f"Token sent to {pubkey}") @@ -430,23 +440,33 @@ async def nostrsend(ctx, amount: int, pubkey: str): @cli.command("nreceive", help="Receive tokens via nostr.") +@click.option( + "--verbose", + "-v", + help="Display more information.", + is_flag=True, + default=False, + type=bool, +) @click.pass_context @coro -async def nostr(ctx): - wallet: Wallet = ctx.obj["WALLET"] +async def nreceive(ctx, verbose: bool): if NOSTR_PRIVATE_KEY is None: print( - "Warning!\n\nYou don't have NOSTR_PRIVATE_KEY set in your .env file. I will create a random private key for this session but I will not remember it. If you lose this key, you will lose access to the DMs you receive on it." + "Warning! You don't have NOSTR_PRIVATE_KEY set in your .env file. I will create a random private key for this session but I will not remember it. If you lose this key, you will lose access to the DMs you receive on it." ) print("") client = NostrClient(privatekey_hex=NOSTR_PRIVATE_KEY) print(f"Your nostr public key: {client.public_key.hex()}") + if verbose: + print(f"Your nostr private key (do not share!): {client.private_key.hex()}") await asyncio.sleep(2) def get_token_callback(event: Event, decrypted_content): - # print( - # f"From {event.public_key[:3]}..{event.public_key[-3:]}: {decrypted_content}" - # ) + if verbose: + print( + f"From {event.public_key[:3]}..{event.public_key[-3:]}: {decrypted_content}" + ) try: # call the receive method asyncio.run(receive(ctx, decrypted_content, "")) From b9123d9bd24a08613776b75dbf098ccb6788877f Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Mon, 26 Dec 2022 17:43:19 +0100 Subject: [PATCH 11/15] add wheel package for secp --- poetry.lock | 17 ++++++++++++++++- pyproject.toml | 1 + requirements.txt | 1 + 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index 27d9a23..f4362d5 100644 --- a/poetry.lock +++ b/poetry.lock @@ -684,6 +684,17 @@ docs = ["Sphinx (>=3.4)", "sphinx-rtd-theme (>=0.5)"] optional = ["python-socks", "wsaccel"] test = ["websockets"] +[[package]] +name = "wheel" +version = "0.38.4" +description = "A built-package format for Python" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.extras] +test = ["pytest (>=3.0.0)"] + [[package]] name = "win32-setctime" version = "1.1.0" @@ -710,7 +721,7 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools" [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "a317c12d282ef30beb2d3ea515e501e78eccfa70cdddc3429ebaf7f743f959bd" +content-hash = "f7238d56229c2e957585fc22331529facc751262430698369fe5a00396344401" [metadata.files] anyio = [ @@ -1230,6 +1241,10 @@ websocket-client = [ {file = "websocket-client-1.3.3.tar.gz", hash = "sha256:d58c5f284d6a9bf8379dab423259fe8f85b70d5fa5d2916d5791a84594b122b1"}, {file = "websocket_client-1.3.3-py3-none-any.whl", hash = "sha256:5d55652dc1d0b3c734f044337d929aaf83f4f9138816ec680c1aefefb4dc4877"}, ] +wheel = [ + {file = "wheel-0.38.4-py3-none-any.whl", hash = "sha256:b60533f3f5d530e971d6737ca6d58681ee434818fab630c83a734bb10c083ce8"}, + {file = "wheel-0.38.4.tar.gz", hash = "sha256:965f5259b566725405b05e7cf774052044b1ed30119b5d586b2703aafe8719ac"}, +] win32-setctime = [ {file = "win32_setctime-1.1.0-py3-none-any.whl", hash = "sha256:231db239e959c2fe7eb1d7dc129f11172354f98361c4fa2d6d2d7e278baa8aad"}, {file = "win32_setctime-1.1.0.tar.gz", hash = "sha256:15cf5750465118d6929ae4de4eb46e8edae9a5634350c01ba582df868e932cb2"}, diff --git a/pyproject.toml b/pyproject.toml index 74f52f7..8745a1b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,7 @@ cryptography = "^38.0.4" websocket-client = "1.3.3" pycryptodomex = "^3.16.0" setuptools = "^65.6.3" +wheel = "^0.38.4" [tool.poetry.dev-dependencies] black = {version = "^22.8.0", allow-prereleases = true} diff --git a/requirements.txt b/requirements.txt index 81e04b2..aeacdf9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -43,5 +43,6 @@ typing-extensions==4.4.0 ; python_version >= "3.7" and python_version < "4.0" urllib3==1.26.13 ; python_version >= "3.7" and python_version < "4.0" uvicorn==0.18.3 ; python_version >= "3.7" and python_version < "4.0" websocket-client==1.3.3 ; python_version >= "3.7" and python_version < "4.0" +wheel==0.38.4 ; python_version >= "3.7" and python_version < "4.0" win32-setctime==1.1.0 ; python_version >= "3.7" and python_version < "4.0" and sys_platform == "win32" zipp==3.11.0 ; python_version >= "3.7" and python_version < "3.8" From b1bb2367408495a5f97bedbfbb5a58682c15cba6 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Mon, 26 Dec 2022 18:21:02 +0100 Subject: [PATCH 12/15] improve help --- cashu/wallet/cli.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cashu/wallet/cli.py b/cashu/wallet/cli.py index 13f9127..6ff24c8 100644 --- a/cashu/wallet/cli.py +++ b/cashu/wallet/cli.py @@ -82,10 +82,13 @@ def cli(ctx, host: str, walletname: str): error_str += "\n\n" if ENV_FILE: error_str += f"Edit your Cashu config file here: {ENV_FILE}" + env_path = ENV_FILE else: error_str += ( f"Ceate a new Cashu config file here: {os.path.join(CASHU_DIR, '.env')}" ) + env_path = os.path.join(CASHU_DIR, ".env") + error_str += f'\n\nYou can turn off Tor with this command: echo "TOR=false" >> {env_path}' raise Exception(error_str) ctx.obj["WALLET"] = wallet From 7c6aaa34a967a9fa078542a1c3f413fc542ce1d9 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Mon, 26 Dec 2022 19:10:55 +0100 Subject: [PATCH 13/15] add help --- cashu/wallet/cli.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cashu/wallet/cli.py b/cashu/wallet/cli.py index 6ff24c8..d14eac5 100644 --- a/cashu/wallet/cli.py +++ b/cashu/wallet/cli.py @@ -409,6 +409,7 @@ async def invoices(ctx): @click.argument( "pubkey", type=str, + help="Nostr pubkey to send tokens to.", ) @click.option( "--verbose", @@ -456,7 +457,7 @@ async def nsend(ctx, amount: int, pubkey: str, verbose: bool): async def nreceive(ctx, verbose: bool): if NOSTR_PRIVATE_KEY is None: print( - "Warning! You don't have NOSTR_PRIVATE_KEY set in your .env file. I will create a random private key for this session but I will not remember it. If you lose this key, you will lose access to the DMs you receive on it." + "Warning: No nostr private key set! You don't have NOSTR_PRIVATE_KEY set in your .env file. I will create a random private key for this session but I will not remember it." ) print("") client = NostrClient(privatekey_hex=NOSTR_PRIVATE_KEY) From a9285e670dd7e5c90a9575175bbb8c12e4d8db53 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Mon, 26 Dec 2022 19:15:03 +0100 Subject: [PATCH 14/15] update config --- .env.example | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.env.example b/.env.example index 0de8074..531d842 100644 --- a/.env.example +++ b/.env.example @@ -32,6 +32,5 @@ LNBITS_ENDPOINT=https://legend.lnbits.com LNBITS_KEY=yourkeyasdasdasd # NOSTR - # this is the hex private key on which you want to receive nostr DMs to -# NOSTR_PRIVATE_KEY=hex_nostrprivatekey_here \ No newline at end of file +NOSTR_PRIVATE_KEY=hex_nostrprivatekey_here \ No newline at end of file From 86d2321fcb606a83e265c15ce976ca0a5c8eafcc Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Mon, 26 Dec 2022 19:23:01 +0100 Subject: [PATCH 15/15] fix cli --- cashu/wallet/cli.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cashu/wallet/cli.py b/cashu/wallet/cli.py index d14eac5..c91b7de 100644 --- a/cashu/wallet/cli.py +++ b/cashu/wallet/cli.py @@ -409,7 +409,6 @@ async def invoices(ctx): @click.argument( "pubkey", type=str, - help="Nostr pubkey to send tokens to.", ) @click.option( "--verbose",