fix: rewrite useCVMClient to use Relay.connect() instead of SimplePool

nostr-tools v2 SimplePool.subscribeMany() callbacks (onevent/oneose) no
longer fire reliably in browser context, causing the CVM client to always
time out. Switch to raw Relay.connect() which uses the v2 API correctly.
Race across all CVM relays and resolve on first valid response.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
This commit is contained in:
shakespeare.diy 2026-06-17 12:10:31 +07:00
parent c772330e74
commit a9c677b58d

View file

@ -23,7 +23,7 @@ import {
generateSecretKey, generateSecretKey,
getPublicKey, getPublicKey,
finalizeEvent, finalizeEvent,
SimplePool, Relay,
} from 'nostr-tools'; } from 'nostr-tools';
import { nip44 } from 'nostr-tools'; import { nip44 } from 'nostr-tools';
@ -113,7 +113,7 @@ export interface CommunityStats {
async function callCVMTool( async function callCVMTool(
toolName: string, toolName: string,
toolArgs: Record<string, unknown>, toolArgs: Record<string, unknown>,
timeoutMs = 10000 timeoutMs = 15000
): Promise<unknown> { ): Promise<unknown> {
// Generate ephemeral keypair for this request // Generate ephemeral keypair for this request
const sk = generateSecretKey(); const sk = generateSecretKey();
@ -138,69 +138,74 @@ async function callCVMTool(
conversationKey conversationKey
); );
// Build the Nostr event // Build and sign the request event
const eventTemplate = { const signedEvent = finalizeEvent({
kind: CVM_REQUEST_KIND, kind: CVM_REQUEST_KIND,
created_at: Math.floor(Date.now() / 1000), created_at: Math.floor(Date.now() / 1000),
tags: [ tags: [['p', CVM_SERVER_PUBKEY]],
['p', CVM_SERVER_PUBKEY],
],
content: encryptedContent, content: encryptedContent,
}; }, sk);
const signedEvent = finalizeEvent(eventTemplate, sk);
const pool = new SimplePool();
// Use raw Relay connections (nostr-tools v2 compatible)
// Try each relay in order, race for the first response
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const relays: InstanceType<typeof Relay>[] = [];
let settled = false;
const cleanup = () => {
relays.forEach(r => { try { r.close(); } catch { /* ignore */ } });
};
const timer = setTimeout(() => { const timer = setTimeout(() => {
sub.close(); if (!settled) {
pool.close(CVM_RELAYS); settled = true;
reject(new Error(`CVM tool call timed out after ${timeoutMs}ms`)); cleanup();
reject(new Error(`CVM tool call timed out after ${timeoutMs}ms`));
}
}, timeoutMs); }, timeoutMs);
// Subscribe for the response before publishing const since = Math.floor(Date.now() / 1000) - 5;
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);
// Match by request ID CVM_RELAYS.forEach(async (url) => {
if (mcpResponse.id !== requestId) return; try {
const relay = await Relay.connect(url);
relays.push(relay);
clearTimeout(timer); // Subscribe for response BEFORE publishing
sub.close(); const sub = relay.subscribe(
pool.close(CVM_RELAYS); [{ 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) { settled = true;
reject(new Error(mcpResponse.error.message ?? 'CVM error')); clearTimeout(timer);
} else { sub.close();
resolve(mcpResponse.result); cleanup();
}
} catch (e) { if (mcpResponse.error) {
// Ignore events we can't decrypt (not for us) 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 // Now publish the request
pool.publish(CVM_RELAYS, signedEvent).catch(() => {}); await relay.publish(signedEvent);
} catch {
// This relay failed — others may still succeed
}
});
}); });
} }