import json import random import time from dataclasses import dataclass from coincurve import PrivateKey from .event_utils import sign_event from .nip44 import nip44_encrypt, nip44_decrypt SEAL_KIND = 13 GIFT_WRAP_KIND = 1059 DM_CHAT_KIND = 14 DM_FILE_KIND = 15 CREATED_AT_JITTER_SEC = 172800 @dataclass class GiftWrapResult: event: dict gift_wrap_pubkey: str @dataclass class UnwrappedDM: pubkey: str kind: int content: str tags: list[list[str]] created_at: int def _jittered_now() -> int: offset = random.randint(-CREATED_AT_JITTER_SEC, CREATED_AT_JITTER_SEC) return int(time.time()) + offset def create_rumor( sender_pk: str, content: str, kind: int = DM_CHAT_KIND, tags: list[list[str]] | None = None, created_at: int | None = None, ) -> dict: return { "pubkey": sender_pk, "created_at": created_at if created_at is not None else int(time.time()), "kind": kind, "tags": tags or [], "content": content, } def create_seal(sender_sk_hex: str, sender_pk: str, receiver_pk: str, rumor: dict) -> dict: if isinstance(sender_sk_hex, str) and len(sender_sk_hex) == 64: sk_bytes = bytes.fromhex(sender_sk_hex) else: sk_bytes = sender_sk_hex if isinstance(sk_bytes, str): sk_bytes = bytes.fromhex(sk_bytes) rumor_json = json.dumps(rumor, separators=(",", ":"), ensure_ascii=False) encrypted_rumor = nip44_encrypt(sk_bytes, receiver_pk, rumor_json) return sign_event( sk=sk_bytes, pubkey=sender_pk, kind=SEAL_KIND, content=encrypted_rumor, tags=[], ) def create_gift_wrap( sender_sk_hex: str, sender_pk: str, receiver_pk: str, content: str, kind: int = DM_CHAT_KIND, tags: list[list[str]] | None = None, ) -> GiftWrapResult: rumor = create_rumor(sender_pk, content, kind, tags) seal = create_seal(sender_sk_hex, sender_pk, receiver_pk, rumor) random_sk = PrivateKey() random_pk_bytes = random_sk.public_key.format(compressed=False)[1:33] random_pk_hex = random_pk_bytes.hex() random_sk_hex = random_sk.secret.hex() if hasattr(random_sk.secret, 'hex') else random_sk.secret if isinstance(random_sk_hex, str): random_sk_bytes = bytes.fromhex(random_sk_hex) else: random_sk_bytes = random_sk_hex seal_json = json.dumps(seal, separators=(",", ":"), ensure_ascii=False) encrypted_seal = nip44_encrypt(random_sk_bytes, receiver_pk, seal_json) wrap_event = sign_event( sk=random_sk_bytes, pubkey=random_pk_hex, kind=GIFT_WRAP_KIND, content=encrypted_seal, tags=[["p", receiver_pk]], ) wrap_event["created_at"] = _jittered_now() return GiftWrapResult(event=wrap_event, gift_wrap_pubkey=random_pk_hex) def unwrap_gift_wrap(sk_bytes: bytes, gift_wrap_event: dict) -> UnwrappedDM | None: try: content = gift_wrap_event.get("content", "") wrap_pubkey = gift_wrap_event.get("pubkey", "") seal_json = nip44_decrypt(sk_bytes, wrap_pubkey, content) seal = json.loads(seal_json) if seal.get("kind") != SEAL_KIND: return None if seal.get("tags"): return None sender_pk = seal.get("pubkey", "") encrypted_rumor = seal.get("content", "") rumor_json = nip44_decrypt(sk_bytes, sender_pk, encrypted_rumor) rumor = json.loads(rumor_json) if rumor.get("pubkey") != sender_pk: return None return UnwrappedDM( pubkey=sender_pk, kind=rumor.get("kind", DM_CHAT_KIND), content=rumor.get("content", ""), tags=rumor.get("tags", []), created_at=rumor.get("created_at", 0), ) except Exception: return None