From f53bc564979619b0dcfe3e68c115d5737607441d Mon Sep 17 00:00:00 2001 From: Avi Date: Wed, 23 Sep 2026 20:31:37 -0500 Subject: [PATCH] fix(nip46): give sign_event the human-approval timeout leash MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Live pairing (Sep 23) proved the signer round-trip works: connect, get_public_key and the first sign_event all succeeded against real Amber. Subsequent signs failed because the client half used the 30s ordinary-RPC leash for sign_event, while every sign waits on a human approving the prompt on the signer's device. The log shows a valid signature arriving ~61s after publication, after the waiter had been removed: 'stale/duplicate response: no waiter ... dropped' — the user watched failures while Amber was signing correctly. sign_event now uses a 120s SIGN_TIMEOUT (same leash as the connect handshake); get_public_key keeps the 30s retry-cadence timeout. --- src/signer/nip46_client.rs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/signer/nip46_client.rs b/src/signer/nip46_client.rs index ec4c3e1..30105ba 100644 --- a/src/signer/nip46_client.rs +++ b/src/signer/nip46_client.rs @@ -38,6 +38,13 @@ const REQUEST_TIMEOUT: Duration = Duration::from_secs(30); /// How long the connect handshake can involve a human approving the app on /// the signer's screen, so it gets a far longer leash than ordinary RPCs. const HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(120); +/// How long a `sign_event` may wait for the signer's answer. Like the +/// handshake, every sign waits for a human to notice and approve the prompt +/// on the signer's device, so it needs the same long leash rather than the +/// 30s ordinary-RPC one: live pairing (Sep 23) showed a perfectly valid +/// signature arriving after the 30s leash expired, discarded as +/// "stale/duplicate response: no waiter" while the user watched failures. +const SIGN_TIMEOUT: Duration = Duration::from_secs(120); /// How long a pairing QR (client-initiated `nostrconnect://`) stays live /// while a human opens their signer and scans it. const PAIRING_TIMEOUT: Duration = Duration::from_secs(300); @@ -1153,12 +1160,16 @@ impl Nip46ClientSigner { /// return [`SigningError`] rather than falling back to any local key. The /// waiter is always removed from the pending map, even on timeout, so a /// late response to an abandoned request finds nothing to wake. + /// + /// Uses the human-approval leash ([`SIGN_TIMEOUT`]), not the 30s + /// ordinary-RPC one: every call here asks a human to approve on the + /// signer's device. async fn send_remote_request( &self, method: &str, params: Vec, ) -> Result { - self.send_rpc(method, params, REQUEST_TIMEOUT, true).await + self.send_rpc(method, params, SIGN_TIMEOUT, true).await } /// Send an encrypted NIP-46 RPC and await its response.