diff --git a/src/hooks/useCVMClient.ts b/src/hooks/useCVMClient.ts index 03b7461..5ec0984 100644 --- a/src/hooks/useCVMClient.ts +++ b/src/hooks/useCVMClient.ts @@ -23,7 +23,7 @@ import { generateSecretKey, getPublicKey, finalizeEvent, - SimplePool, + Relay, } from 'nostr-tools'; import { nip44 } from 'nostr-tools'; @@ -113,7 +113,7 @@ export interface CommunityStats { async function callCVMTool( toolName: string, toolArgs: Record, - timeoutMs = 10000 + timeoutMs = 15000 ): Promise { // Generate ephemeral keypair for this request const sk = generateSecretKey(); @@ -138,69 +138,74 @@ async function callCVMTool( conversationKey ); - // Build the Nostr event - const eventTemplate = { + // Build and sign the request event + const signedEvent = finalizeEvent({ kind: CVM_REQUEST_KIND, created_at: Math.floor(Date.now() / 1000), - tags: [ - ['p', CVM_SERVER_PUBKEY], - ], + tags: [['p', CVM_SERVER_PUBKEY]], content: encryptedContent, - }; - - const signedEvent = finalizeEvent(eventTemplate, sk); - - const pool = new SimplePool(); + }, sk); + // Use raw Relay connections (nostr-tools v2 compatible) + // Try each relay in order, race for the first response return new Promise((resolve, reject) => { + const relays: InstanceType[] = []; + let settled = false; + + const cleanup = () => { + relays.forEach(r => { try { r.close(); } catch { /* ignore */ } }); + }; + const timer = setTimeout(() => { - sub.close(); - pool.close(CVM_RELAYS); - reject(new Error(`CVM tool call timed out after ${timeoutMs}ms`)); + if (!settled) { + settled = true; + cleanup(); + reject(new Error(`CVM tool call timed out after ${timeoutMs}ms`)); + } }, timeoutMs); - // Subscribe for the response before publishing - const sub = pool.subscribeMany( - CVM_RELAYS, - [ - { - kinds: [CVM_RESPONSE_KIND], - '#p': [pk], // response tagged to our ephemeral pubkey - since: Math.floor(Date.now() / 1000) - 5, - }, - ], - { - onevent(responseEvent) { - try { - // Decrypt the response - const convKey = nip44.v2.utils.getConversationKey(sk, responseEvent.pubkey); - const decrypted = nip44.v2.decrypt(responseEvent.content, convKey); - const mcpResponse = JSON.parse(decrypted); + const since = Math.floor(Date.now() / 1000) - 5; - // Match by request ID - if (mcpResponse.id !== requestId) return; + CVM_RELAYS.forEach(async (url) => { + try { + const relay = await Relay.connect(url); + relays.push(relay); - clearTimeout(timer); - sub.close(); - pool.close(CVM_RELAYS); + // Subscribe for response BEFORE publishing + const sub = relay.subscribe( + [{ kinds: [CVM_RESPONSE_KIND], '#p': [pk], since }], + { + onevent(responseEvent) { + if (settled) return; + try { + const convKey = nip44.v2.utils.getConversationKey(sk, responseEvent.pubkey); + const decrypted = nip44.v2.decrypt(responseEvent.content, convKey); + const mcpResponse = JSON.parse(decrypted); + if (mcpResponse.id !== requestId) return; - if (mcpResponse.error) { - reject(new Error(mcpResponse.error.message ?? 'CVM error')); - } else { - resolve(mcpResponse.result); - } - } catch (e) { - // Ignore events we can't decrypt (not for us) + settled = true; + clearTimeout(timer); + sub.close(); + cleanup(); + + if (mcpResponse.error) { + reject(new Error(mcpResponse.error.message ?? 'CVM error')); + } else { + resolve(mcpResponse.result); + } + } catch { + // Not for us — ignore + } + }, } - }, - oneose() { - // Keep listening even after EOSE — response may come after - }, - } - ); + ); - // Publish the request after subscribing - pool.publish(CVM_RELAYS, signedEvent).catch(() => {}); + // Now publish the request + await relay.publish(signedEvent); + } catch { + // This relay failed — others may still succeed + } + }); }); }