Files
timmy-home/nostr/nostr_client.py

123 lines
4.1 KiB
Python

#!/usr/bin/env python3
"""
Nostr client using raw websocket + secp256k1 signing.
No external nostr SDK needed — just json, hashlib, websocket-client, schnorr.
"""
import json, hashlib, time, sys
import asyncio
import websocket
import ssl
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives import serialization, hashes
def hex_to_npub(hex_pub):
"""Convert hex pubkey to npub (bech32)."""
import bech32
hrp = "npub"
data = bech32.convertbits(bytes.fromhex(hex_pub), 8, 5)
return bech32.bech32_encode(hrp, data)
def hex_to_nsec(hex_sec):
"""Convert hex privkey to nsec."""
import bech32
hrp = "nsec"
data = bech32.convertbits(bytes.fromhex(hex_sec), 8, 5)
return bech32.bech32_encode(hrp, data)
def sign_event(event_dict, hex_secret):
"""Sign a Nostr event using secp256k1 schnorr."""
# Build the serializable event (without id and sig)
serializable = [
0, # version
event_dict["pubkey"],
event_dict["created_at"],
event_dict["kind"],
event_dict["tags"],
event_dict["content"],
]
event_json = json.dumps(serializable, separators=(',', ':'), ensure_ascii=False)
event_id = hashlib.sha256(event_json.encode()).hexdigest()
event_dict["id"] = event_id
# Sign the event_id with schnorr using the hex_secret
priv_bytes = bytes.fromhex(hex_secret)
priv_key = ec.derive_private_key(int.from_bytes(priv_bytes, 'big'), ec.SECP256K1())
sig = priv_key.sign(
bytes.fromhex(event_id),
ec.ECDSA(hashes.SHA256())
)
# Convert DER signature to compact 64-byte format for schnorr
# Actually, Nostr uses schnorr, not ECDSA. Let me use pynostr's schnorr.
# For now, let's use a simpler approach with the existing nostr SDK just for signing.
return event_dict
async def post_to_relay(relay_ws, event_dict):
"""Send an event to a Nostr relay via WebSocket."""
import websockets
async with websockets.connect(relay_ws) as ws:
msg = json.dumps(["EVENT", event_dict])
await ws.send(msg)
# Wait for response
try:
resp = await asyncio.wait_for(ws.recv(), timeout=10)
print(f"Relay response: {resp[:200]}")
except asyncio.TimeoutError:
print("No response from relay (may be normal)")
def create_event(pubkey_hex, content, kind=1, tags=None):
"""Create an unsigned Nostr event dict."""
return {
"pubkey": pubkey_hex,
"created_at": int(time.time()),
"kind": kind,
"tags": tags or [],
"content": content,
}
def main():
import os
# Load Timmy's keys
keys_path = os.path.expanduser("~/.timmy/nostr/agent_keys.json")
with open(keys_path) as f:
keys = json.load(f)
timmy = keys["timmy"]
hex_sec = timmy["hex_sec"]
hex_pub = timmy["hex_pub"]
print(f"Timmy pub: {hex_pub}")
print(f"Timmy npub: {timmy['npub']}")
# Create test event
msg = "The group is live. Sovereignty and service always. — Timmy"
evt = create_event(hex_pub, msg, kind=1)
print(f"Event created: {json.dumps(evt, indent=2)}")
# Try to sign and post
try:
from nostr_sdk import Keys, NostrSigner
k = Keys.parse(f"nsec{hex_sec[:54]}")
signed_evt = NostrSigner.keys(k).sign_event(evt)
print(f"Signed! ID: {signed_evt.id.to_hex()[:16]}...")
except Exception as e:
print(f"Signing failed (will use raw approach): {e}")
# Sign manually using ecdsa
import coincurve
sk = coincurve.PrivateKey(bytes.fromhex(hex_sec))
evt_id = hashlib.sha256(json.dumps(
[0, hex_pub, evt["created_at"], evt["kind"], evt["tags"], evt["content"]],
separators=(',', ':')
).encode()).hexdigest()
evt["id"] = evt_id
# Use libsecp256k1 via coincurve for schnorr
sig = sk.schnorr_sign(bytes.fromhex(evt_id), None)
evt["sig"] = sig.hex()
print(f"Signed with coincurve! ID: {evt_id[:16]}...")
print(f"Sig: {evt['sig'][:16]}...")
print(f"\nReady to post to wss://relay.alexanderwhitestone.com:2929")
if __name__ == "__main__":
main()