123 lines
4.1 KiB
Python
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()
|