From f261fe974fea8da9d1e70c0265a5ce31dfcdac6e Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 15 Jan 2026 15:56:18 +0000 Subject: [PATCH] Add relay-specific token fetching for NIP-60 wallets Implement fetching token events from wallet-configured relays: - Add fetchTokensFromRelays() function to query specific relays - Use pool.subscription() with eventStore integration - After unlocking wallet config, fetch tokens from wallet.relays if present - Fall back to default timeline if no wallet relays configured - Add detailed logging for relay fetching and decryption - Handle EOSE events and subscription cleanup - 10 second timeout for token fetching This allows wallets to specify their preferred relays for token storage according to NIP-60 spec, enabling proper balance calculation from the correct relay sources. According to NIP-60, clients should: 1. Use relays specified in wallet config (wallet.relays) 2. Fall back to NIP-65 relay lists if not specified 3. Query for kind:7375 token events from those relays --- src/components/WalletViewer.tsx | 152 ++++++++++++++++++++++++++------ 1 file changed, 127 insertions(+), 25 deletions(-) diff --git a/src/components/WalletViewer.tsx b/src/components/WalletViewer.tsx index b678683..a09901d 100644 --- a/src/components/WalletViewer.tsx +++ b/src/components/WalletViewer.tsx @@ -23,6 +23,8 @@ import { import { Alert, AlertDescription } from "@/components/ui/alert"; import { useMemo, useState, useCallback } from "react"; import accountManager from "@/services/accounts"; +import pool from "@/services/relay-pool"; +import type { NostrEvent } from "nostr-tools"; import { decryptWalletConfig, decryptUnspentTokens, @@ -110,6 +112,66 @@ export function WalletViewer({ pubkey }: WalletViewerProps) { // Get active account from accountManager const activeAccount = use$(accountManager.active$); + // Fetch token events from specific relays + const fetchTokensFromRelays = useCallback( + async (relays: string[], pubkey: string) => { + console.log( + `[WalletViewer] Fetching tokens from ${relays.length} relay(s)...`, + ); + + return new Promise((resolve) => { + const events: NostrEvent[] = []; + const timeout = setTimeout(() => { + console.log( + `[WalletViewer] Fetch timeout, got ${events.length} events`, + ); + resolve(events); + }, 10000); // 10 second timeout + + const observable = pool.subscription( + relays, + [ + { + kinds: [7375], + authors: [pubkey], + }, + ], + { + eventStore: eventStore, + }, + ); + + const subscription = observable.subscribe({ + next: (event: NostrEvent | string) => { + if (typeof event === "string") { + // EOSE marker + if (event === "EOSE") { + console.log( + `[WalletViewer] EOSE received, got ${events.length} total events`, + ); + clearTimeout(timeout); + subscription.unsubscribe(); + resolve(events); + } + } else { + console.log( + "[WalletViewer] Got token event from relay:", + event.id, + ); + events.push(event); + } + }, + error: (error) => { + console.error("[WalletViewer] Subscription error:", error); + clearTimeout(timeout); + resolve(events); + }, + }); + }); + }, + [eventStore], + ); + // Manual decrypt function const decryptWalletData = useCallback(async () => { if (!activeAccount?.nip44) { @@ -130,48 +192,88 @@ export function WalletViewer({ pubkey }: WalletViewerProps) { try { // Decrypt wallet config + let config: WalletConfig | null = null; if (walletConfigEvent) { console.log("[WalletViewer] Decrypting wallet config..."); - const config = await decryptWalletConfig( - walletConfigEvent, - activeAccount, - ); + config = await decryptWalletConfig(walletConfigEvent, activeAccount); if (config) { console.log( "[WalletViewer] Wallet config decrypted successfully:", config, ); setWalletConfig(config); + + // If wallet has specific relays configured, fetch token events from those relays + if (config.relays && config.relays.length > 0 && resolvedPubkey) { + console.log( + `[WalletViewer] Fetching token events from ${config.relays.length} wallet relay(s)...`, + ); + const fetchedEvents = await fetchTokensFromRelays( + config.relays, + resolvedPubkey, + ); + console.log( + `[WalletViewer] Fetched ${fetchedEvents.length} token event(s) from wallet relays`, + ); + + // Decrypt the fetched events + if (fetchedEvents.length > 0) { + const decryptedTokens: UnspentTokens[] = []; + for (const event of fetchedEvents) { + const tokens = await decryptUnspentTokens(event, activeAccount); + if (tokens) { + console.log("[WalletViewer] Token event decrypted:", tokens); + decryptedTokens.push(tokens); + } else { + console.warn( + "[WalletViewer] Token event decryption returned null:", + event.id, + ); + } + } + setUnspentTokens(decryptedTokens); + console.log( + `[WalletViewer] Total ${decryptedTokens.length} token event(s) decrypted from wallet relays`, + ); + } + } } else { console.warn("[WalletViewer] Wallet config decryption returned null"); } } - // Decrypt token events - if (tokenEvents && tokenEvents.length > 0) { + // Fall back to decrypting token events from default timeline if no wallet relays + if (!config || !config.relays || config.relays.length === 0) { console.log( - `[WalletViewer] Decrypting ${tokenEvents.length} token event(s)...`, + "[WalletViewer] No wallet relays, using default token events", ); - const decryptedTokens: UnspentTokens[] = []; - for (const event of tokenEvents) { - const tokens = await decryptUnspentTokens(event, activeAccount); - if (tokens) { - console.log("[WalletViewer] Token event decrypted:", tokens); - decryptedTokens.push(tokens); - } else { - console.warn( - "[WalletViewer] Token event decryption returned null:", - event.id, - ); + + // Decrypt token events from default timeline + if (tokenEvents && tokenEvents.length > 0) { + console.log( + `[WalletViewer] Decrypting ${tokenEvents.length} token event(s)...`, + ); + const decryptedTokens: UnspentTokens[] = []; + for (const event of tokenEvents) { + const tokens = await decryptUnspentTokens(event, activeAccount); + if (tokens) { + console.log("[WalletViewer] Token event decrypted:", tokens); + decryptedTokens.push(tokens); + } else { + console.warn( + "[WalletViewer] Token event decryption returned null:", + event.id, + ); + } } + setUnspentTokens(decryptedTokens); + console.log( + `[WalletViewer] Total ${decryptedTokens.length} token event(s) decrypted`, + ); + } else { + console.log("[WalletViewer] No token events to decrypt"); + setUnspentTokens([]); } - setUnspentTokens(decryptedTokens); - console.log( - `[WalletViewer] Total ${decryptedTokens.length} token event(s) decrypted`, - ); - } else { - console.log("[WalletViewer] No token events to decrypt"); - setUnspentTokens([]); } // Decrypt history events