#!/usr/bin/env python3 """ Publish a test App Stack (kind 30267) for "Nostr Clients" using n_signer via qrexec. Usage: python3 app-stacks/publish_test_stack.py """ import asyncio import json import struct import subprocess import sys import time import websockets QREXEC_TARGET = "nostr_signer" QREXEC_SERVICE = "qubes.NsignerRpc" RELAY = "wss://relay.zapstore.dev" # Zapstore community pubkey — required in the `h` tag for stacks to appear # in the Zapstore Android app. See app-stacks/README.md for details. KZAPSTORE_COMMUNITY_PUBKEY = "acfeaea6e51420e8068fac446ca9d17d7a9ef6a5d20d93894e50fee3d4902a84" def qrexec_call(request): """Send a framed JSON-RPC request via qrexec and return the response.""" framed = struct.pack(">I", len(json.dumps(request).encode())) + json.dumps(request).encode() proc = subprocess.run( ["qrexec-client-vm", QREXEC_TARGET, QREXEC_SERVICE], input=framed, capture_output=True, timeout=30 ) if proc.returncode != 0: raise Exception(f"qrexec failed: {proc.stderr.decode()}") resp_len = struct.unpack(">I", proc.stdout[:4])[0] return json.loads(proc.stdout[4:4 + resp_len]) async def publish_event(signed_event): """Publish a signed event to the relay.""" async with websockets.connect(RELAY, max_size=10_000_000, open_timeout=10) as ws: msg = json.dumps(["EVENT", signed_event]) await ws.send(msg) while True: try: resp = await asyncio.wait_for(ws.recv(), timeout=10) data = json.loads(resp) if data[0] == "OK" and data[1] == signed_event["id"]: accepted = data[2] msg = data[3] if len(data) > 3 else "" print(f" Relay: {'✅ ACCEPTED' if accepted else '❌ REJECTED'}: {msg}") return accepted except asyncio.TimeoutError: print(" No OK response from relay") return False async def main(): print("=" * 60) print("Publishing App Stack: Nostr Clients") print("=" * 60) # Step 1: Get pubkey for nostr_index 0 print("\n1. Getting pubkey from n_signer...") req = {"id": "1", "method": "nostr_get_public_key", "params": [{"nostr_index": 0}]} resp = qrexec_call(req) if "error" in resp: print(f" Error: {resp['error']}") sys.exit(1) pubkey = resp["result"] print(f" Pubkey: {pubkey}") # Step 2: Build the unsigned event created_at = int(time.time()) event = { "kind": 30267, "content": "", # Must be empty — zapstore treats non-empty content as encrypted "tags": [ ["d", "privacy-approved-nostr-clients"], ["t", "app-stack"], ["name", "Privacy Approved — Nostr Clients"], ["description", "Nostr-native social and communication clients that passed privacy review. No tracking SDKs, minimal permissions, open source."], ["f", "android-arm64-v8a"], ["h", KZAPSTORE_COMMUNITY_PUBKEY], ["a", "32267:aa9047325603dacd4f8142093567973566de3b1e20a89557b728c3be4c6a844b:com.vitorpamplona.amethyst", "wss://relay.zapstore.dev"], ["a", "32267:78ce6faa72264387284e647ba6938995735ec8c7d5c5a65737e55130f026307d:com.greenart7c3.nostrsigner", "wss://relay.zapstore.dev"], ["a", "32267:e2ccf7cf20403f3f2a4a55b328f0de3be38558a7d5f33632fdaaefc726c1c8eb:com.wisp.app", "wss://relay.zapstore.dev"], ["a", "32267:e2ccf7cf20403f3f2a4a55b328f0de3be38558a7d5f33632fdaaefc726c1c8eb:com.darkwisp.app", "wss://relay.zapstore.dev"], ["a", "32267:75d737c3472471029c44876b330d2284288a42779b591a2ed4daa1c6c07efaf7:org.parres.whitenoise", "wss://relay.zapstore.dev"], ["a", "32267:f55678aa1f5d554536d456b13beab04f636d63fdedd586fe38a4cb9ce48c90bc:com.nospeak.app", "wss://relay.zapstore.dev"], ["a", "32267:b2cdcb37d32533145c00c4f43d5e1e1deb7c67bceea7ef63f526ca4cab891633:org.nostr.nostrord", "wss://relay.zapstore.dev"], ["a", "32267:85df00a2f6a91845354c8d2d9fbab4002bb85b4225baeab60fafb2587c5038ea:com.shosho.app", "wss://relay.zapstore.dev"], ["a", "32267:7d33ba57d8a6e8869a1f1d5215254597594ac0dbfeb01b690def8c461b82db35:com.traveltelly.app", "wss://relay.zapstore.dev"], ["a", "32267:b22b06b051fd5232966a9344a634d956c3dc33a7f5ecdcad9ed11ddc4120a7f2:app.nostrmail.client", "wss://relay.zapstore.dev"], ["a", "32267:c4a39f1291291d452405cd8ddd798c4a29a3858c52cd0d843f1f6852cf17682e:co.openvine.app", "wss://relay.zapstore.dev"], ["a", "32267:20986fb83e775d96d188ca5c9df10ce6d613e0eb7e5768a0f0b12b37cdac21b3:com.yakihonne.yakihonne", "wss://relay.zapstore.dev"], ["a", "32267:3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24:com.nostrnests.app", "wss://relay.zapstore.dev"], ["a", "32267:d1bd33333733dcc411f0ee893b38b8522fc0de227fff459d99044ced9e65581b:app.nostria.twa", "wss://relay.zapstore.dev"], ["a", "32267:d1bd33333733dcc411f0ee893b38b8522fc0de227fff459d99044ced9e65581b:app.nostria", "wss://relay.zapstore.dev"], ["a", "32267:78ce6faa72264387284e647ba6938995735ec8c7d5c5a65737e55130f026307d:dev.zapstore.alpha", "wss://relay.zapstore.dev"], ["a", "32267:d70b722b67768cbf1e3ac777e2107bbc447a9768899b548bf694f71a2a6b52f8:com.pearcal", "wss://relay.zapstore.dev"], ["a", "32267:d70b722b67768cbf1e3ac777e2107bbc447a9768899b548bf694f71a2a6b52f8:com.pearcircle", "wss://relay.zapstore.dev"], ["a", "32267:781a1527055f74c1f70230f10384609b34548f8ab6a0a6caa74025827f9fdae5:pub.ditto.app", "wss://relay.zapstore.dev"], ["a", "32267:97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322:social.flotilla", "wss://relay.zapstore.dev"], ], "created_at": created_at, "pubkey": pubkey, } print(f"\n2. Signing event (kind 30267, d=privacy-approved-nostr-clients)...") req = { "id": "2", "method": "nostr_sign_event", "params": [json.dumps(event), {"nostr_index": 0}] } resp = qrexec_call(req) if "error" in resp: print(f" Error: {resp['error']}") sys.exit(1) signed_event = json.loads(resp["result"]) print(f" Event ID: {signed_event['id']}") print(f" Signature: {signed_event['sig'][:20]}...") # Step 3: Publish to relay print(f"\n3. Publishing to {RELAY}...") accepted = await publish_event(signed_event) if accepted: print(f"\n✅ Published successfully!") print(f" Stack: privacy-approved-nostr-clients") print(f" Apps: 19 Nostr clients") else: print(f"\n❌ Failed to publish") # Save the signed event with open("app-stacks/signed_stack_event.json", "w") as f: json.dump(signed_event, f, indent=2) print(f"\n Signed event saved to app-stacks/signed_stack_event.json") if __name__ == "__main__": asyncio.run(main())