diff --git a/Cargo.toml b/Cargo.toml index 4e15e5b..431527f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ rpassword = "7" sha2 = "0.10" async-trait = "0.1" keyring = "4.2" +futures-util = "0.3" [dev-dependencies] base64 = "0.22" diff --git a/src/signer/nip46_client.rs b/src/signer/nip46_client.rs index c413aa0..628374b 100644 --- a/src/signer/nip46_client.rs +++ b/src/signer/nip46_client.rs @@ -870,6 +870,14 @@ impl Nip46ClientSigner { conversation: ConversationKey, client: Client, ) -> Result<(), String> { + // Subscribe BEFORE the handshake publishes anything: relays only push + // events to subscriptions that exist at delivery time, and the + // identity RPC (`get_public_key`) fires within milliseconds of this + // point. When the spawn raced ahead of the subscription, Amber's fast + // reply landed in the gap and was lost — the live Sep 23 scan died + // exactly there ("signer would not reveal its public key" after a + // clean `connect` + secret echo). + let (notifications, subscription) = self.subscribe_to_signer(&client, uri.peer).await?; { let handshake = self.clone(); let peer = uri.peer; @@ -879,7 +887,31 @@ impl Nip46ClientSigner { } }); } - self.run_demux(uri, keys, conversation, client).await + self.run_demux(uri, keys, conversation, client, notifications, subscription) + .await + } + + /// Open the notification stream and register the kind-24133 author + /// subscription for the signer. Kept separate so every connect path can + /// establish it BEFORE publishing its first RPC. + async fn subscribe_to_signer( + &self, + client: &Client, + peer: PublicKey, + ) -> Result< + ( + std::pin::Pin + Send>>, + Output, + ), + String, + > { + let filter = Filter::new().kind(Kind::NostrConnect).author(peer); + let notifications = client.notifications(); + let subscription = client + .subscribe(filter) + .await + .map_err(|e| format!("Could not subscribe: {e}"))?; + Ok((notifications, subscription)) } /// Get current connection status. @@ -1177,6 +1209,12 @@ impl Nip46ClientSigner { // Update connected relays self.inner.lock().await.client = Some(client.clone()); + // Subscribe BEFORE the handshake publishes `connect`: relays only + // push the signer's replies to subscriptions that exist at delivery + // time, so registering after the first publish can silently drop the + // ack (see run_paired for the live incident this fixes). + let (notifications, subscription) = self.subscribe_to_signer(&client, uri.peer).await?; + // The connect handshake runs as its own task: it publishes `connect` // and then must AWAIT the signer's ack — which can wait on a human // approving us in Amber — followed by `get_public_key` to learn the @@ -1195,7 +1233,8 @@ impl Nip46ClientSigner { }); } - self.run_demux(uri, keys, conversation, client).await + self.run_demux(uri, keys, conversation, client, notifications, subscription) + .await } /// The long-lived request/response demux loop, shared by both connect @@ -1209,15 +1248,11 @@ impl Nip46ClientSigner { keys: Keys, conversation: ConversationKey, client: Client, + mut notifications: std::pin::Pin< + Box + Send>, + >, + subscription: Output, ) -> Result<(), String> { - // Subscribe to kind 24133 from signer - let filter = Filter::new().kind(Kind::NostrConnect).author(uri.peer); - let mut notifications = client.notifications(); - let subscription = client - .subscribe(filter) - .await - .map_err(|e| format!("Could not subscribe: {e}"))?; - // Handle incoming requests loop { let incoming =