diff --git a/README.md b/README.md
index e331047..a2aae66 100644
--- a/README.md
+++ b/README.md
@@ -62,6 +62,15 @@ This template adds the following dependencies to base MKStack:
}
```
+### Router Implementation
+
+This template includes a custom Router class that bridges the version gap between `@welshman/util` v0.4.2 and the v0.0.2 interface that NWelshman expects. The implementation provides:
+
+- Full compatibility with NWelshman's routing requirements
+- Support for all Router scenario methods (`User()`, `FromPubkeys()`, `WithinMultipleContexts()`, etc.)
+- Proper relay selection and quality scoring
+- Policy-based fallback handling
+
## Usage
The template maintains full compatibility with the base MKStack API. All existing hooks and components work without modification:
diff --git a/index.html b/index.html
index cd5ada1..56f3628 100644
--- a/index.html
+++ b/index.html
@@ -1,8 +1,13 @@
+ Nostr Client
+
+
+
+
diff --git a/public/manifest.webmanifest b/public/manifest.webmanifest
new file mode 100644
index 0000000..8de24da
--- /dev/null
+++ b/public/manifest.webmanifest
@@ -0,0 +1,16 @@
+{
+ "name": "Nostr Client",
+ "short_name": "Nostr",
+ "description": "A modern Nostr client application built with React, TailwindCSS, and Nostrify with Welshman routing",
+ "start_url": "/",
+ "display": "standalone",
+ "background_color": "#ffffff",
+ "theme_color": "#000000",
+ "icons": [
+ {
+ "src": "/favicon.ico",
+ "sizes": "any",
+ "type": "image/x-icon"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/src/components/NostrProvider.integration.test.tsx b/src/components/NostrProvider.integration.test.tsx
index b7dcb7e..94f640f 100644
--- a/src/components/NostrProvider.integration.test.tsx
+++ b/src/components/NostrProvider.integration.test.tsx
@@ -1,281 +1,288 @@
-import { describe, it, expect, beforeAll } from 'vitest';
-import { NWelshman } from '@nostrify/welshman';
+import React from 'react';
+import { describe, it, expect } from 'vitest';
+import { render, waitFor } from '@testing-library/react';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { useNostr } from '@nostrify/react';
+import NostrProvider from './NostrProvider';
+import { AppProvider } from '@/components/AppProvider';
+import { AppConfig } from '@/contexts/AppContext';
import type { NostrEvent } from '@nostrify/nostrify';
-// Use Nostrify's event creation instead of nostr-tools
-function createTestEvent(): NostrEvent {
- return {
- id: 'test-' + Date.now(),
- pubkey: '0'.repeat(64),
- created_at: Math.floor(Date.now() / 1000),
- kind: 1,
- tags: [['client', 'nostrify-welshman-test']],
- content: `Integration test from Nostrify NWelshman - ${Date.now()}`,
- sig: '0'.repeat(128),
- };
-}
+// Test configuration using production relays
+const testConfig: AppConfig = {
+ theme: "light",
+ relayUrl: "wss://relay.damus.io",
+};
-// Router configuration to match what we use in NostrProvider
-interface RouterOptions {
- getUserPubkey: () => string | null;
- getGroupRelays: (address: string) => string[];
- getCommunityRelays: (address: string) => string[];
- getPubkeyRelays: (pubkey: string, mode?: unknown) => string[];
- getStaticRelays: () => string[];
- getIndexerRelays: () => string[];
- getSearchRelays: () => string[];
- getRelayQuality: (url: string) => number;
- getRedundancy: () => number;
- getLimit: () => number;
-}
-
-class Router {
- constructor(public options: RouterOptions) {}
-
- // Add the methods that NWelshman expects based on the source code
- User() {
- return {
- policy: (fn: any) => ({
- getUrls: () => this.options.getStaticRelays()
- }),
- getUrls: () => this.options.getStaticRelays()
- };
- }
-
- FromPubkeys(pubkeys: string[]) {
- return {
- policy: (fn: any) => ({
- getSelections: () => this.options.getStaticRelays().map(relay => ({
- relay,
- values: pubkeys
- }))
- })
- };
- }
-
- WithinMultipleContexts(contexts: string[]) {
- return {
- policy: (fn: any) => ({
- getSelections: () => this.options.getStaticRelays().map(relay => ({
- relay,
- values: contexts
- }))
- })
- };
- }
-
- PublishEvent(event: any) {
- return {
- getUrls: () => this.options.getStaticRelays()
- };
- }
-
- product(filters: string[], relays: string[]) {
- return {
- getSelections: () => relays.map(relay => ({
- relay,
- values: filters
- }))
- };
- }
-
- merge(scenarios: any[]) {
- return {
- getSelections: () => this.options.getStaticRelays().map(relay => ({
- relay,
- values: ['test']
- }))
- };
- }
-
- addMinimalFallbacks = (count: number, limit: number) => {
- return Math.min(count + 1, limit);
- };
-}
-
-const testRelays = [
- 'wss://relay.nostr.band',
- 'wss://relay.damus.io',
+const presetRelays = [
+ { url: 'wss://relay.nostr.band', name: 'Nostr.Band' },
+ { url: 'wss://relay.primal.net', name: 'Primal' },
];
-describe('NWelshman Integration Tests', () => {
- let pool: NWelshman;
- let testRouter: Router;
+// Test event from the user
+const TEST_EVENT_ID = '5c3d5d419619363d8b34b6fced33c98ffb3e156062ef7047da908dd97f061da1';
+const TEST_AUTHOR_PUBKEY = '781a1527055f74c1f70230f10384609b34548f8ab6a0a6caa74025827f9fdae5';
- beforeAll(() => {
- testRouter = new Router({
- getUserPubkey: () => null,
- getGroupRelays: () => [],
- getCommunityRelays: () => [],
- getPubkeyRelays: () => testRelays,
- getStaticRelays: () => testRelays,
- getIndexerRelays: () => testRelays,
- getSearchRelays: () => testRelays,
- getRelayQuality: (url: string) => {
- if (testRelays.includes(url)) return 1.0;
- return 0.5;
- },
- getRedundancy: () => 2,
- getLimit: () => 5,
- });
-
- pool = new NWelshman(testRouter as unknown as import('@welshman/util').Router);
- });
-
- it('should query events from real relays', async () => {
- // Query for some recent kind 1 events (text notes)
- const events = await pool.query([
- {
- kinds: [1],
- limit: 3,
+// Test component that uses the NostrProvider
+function TestNostrQuery({ onResult }: { onResult: (events: NostrEvent[]) => void }) {
+ const { nostr } = useNostr();
+
+ React.useEffect(() => {
+ const runQuery = async () => {
+ try {
+ const events = await nostr.query([
+ {
+ ids: [TEST_EVENT_ID],
+ }
+ ], {
+ signal: AbortSignal.timeout(10000),
+ });
+ onResult(events);
+ } catch (error) {
+ console.error('Query failed:', error);
+ onResult([]);
}
- ], {
- signal: AbortSignal.timeout(10000), // 10 second timeout
+ };
+
+ runQuery();
+ }, [nostr, onResult]);
+
+ return Testing Nostr Query
;
+}
+
+function TestRecentEvents({ onResult }: { onResult: (events: NostrEvent[]) => void }) {
+ const { nostr } = useNostr();
+
+ React.useEffect(() => {
+ const runQuery = async () => {
+ try {
+ const events = await nostr.query([
+ {
+ kinds: [1],
+ limit: 3,
+ }
+ ], {
+ signal: AbortSignal.timeout(10000),
+ });
+ onResult(events);
+ } catch (error) {
+ console.error('Query failed:', error);
+ onResult([]);
+ }
+ };
+
+ runQuery();
+ }, [nostr, onResult]);
+
+ return Testing Recent Events
;
+}
+
+function TestAuthorMetadata({ onResult }: { onResult: (events: NostrEvent[]) => void }) {
+ const { nostr } = useNostr();
+
+ React.useEffect(() => {
+ const runQuery = async () => {
+ try {
+ const events = await nostr.query([
+ {
+ kinds: [0],
+ authors: [TEST_AUTHOR_PUBKEY],
+ }
+ ], {
+ signal: AbortSignal.timeout(10000),
+ });
+ onResult(events);
+ } catch (error) {
+ console.error('Query failed:', error);
+ onResult([]);
+ }
+ };
+
+ runQuery();
+ }, [nostr, onResult]);
+
+ return Testing Author Metadata
;
+}
+
+describe('NostrProvider Integration Tests', () => {
+ const createTestWrapper = () => {
+ const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: { retry: false },
+ mutations: { retry: false },
+ },
});
- expect(events).toBeDefined();
- expect(Array.isArray(events)).toBe(true);
-
- // We might get 0 events if relays are slow, but the structure should be correct
- if (events.length > 0) {
- const event = events[0];
- expect(event).toHaveProperty('id');
- expect(event).toHaveProperty('pubkey');
- expect(event).toHaveProperty('created_at');
- expect(event).toHaveProperty('kind');
- expect(event).toHaveProperty('tags');
- expect(event).toHaveProperty('content');
- expect(event).toHaveProperty('sig');
- expect(event.kind).toBe(1);
- }
- }, 15000); // 15 second timeout for this test
+ return ({ children }: { children: React.ReactNode }) => (
+
+
+
+ {children}
+
+
+
+ );
+ };
- it('should handle req stream from real relays', async () => {
- const events: NostrEvent[] = [];
+ it('should query the specific test event from real relays', async () => {
+ let receivedEvents: NostrEvent[] = [];
+
+ const TestWrapper = createTestWrapper();
+
+ render(
+
+ { receivedEvents = events; }} />
+
+ );
+
+ // Wait for the query to complete
+ await waitFor(() => {
+ expect(receivedEvents.length).toBeGreaterThan(0);
+ }, { timeout: 15000 });
+
+ // Verify we got the correct event
+ expect(receivedEvents).toBeDefined();
+ expect(Array.isArray(receivedEvents)).toBe(true);
+
+ const event = receivedEvents[0];
+ expect(event.id).toBe(TEST_EVENT_ID);
+ expect(event.pubkey).toBe(TEST_AUTHOR_PUBKEY);
+ expect(event.kind).toBe(1);
+ expect(event.content).toBeDefined();
+ expect(event.sig).toBeDefined();
+ }, 20000);
+
+ it('should query recent kind 1 events from real relays', async () => {
+ let receivedEvents: NostrEvent[] = [];
+
+ const TestWrapper = createTestWrapper();
+
+ render(
+
+ { receivedEvents = events; }} />
+
+ );
+
+ // Wait for the query to complete
+ await waitFor(() => {
+ expect(receivedEvents.length).toBeGreaterThan(0);
+ }, { timeout: 15000 });
+
+ // Verify we got valid events
+ expect(receivedEvents).toBeDefined();
+ expect(Array.isArray(receivedEvents)).toBe(true);
+
+ const event = receivedEvents[0];
+ expect(event).toHaveProperty('id');
+ expect(event).toHaveProperty('pubkey');
+ expect(event).toHaveProperty('created_at');
+ expect(event).toHaveProperty('kind');
+ expect(event).toHaveProperty('tags');
+ expect(event).toHaveProperty('content');
+ expect(event).toHaveProperty('sig');
+ expect(event.kind).toBe(1);
+ }, 20000);
+
+ it('should query metadata for the test author from indexer relays', async () => {
+ let receivedEvents: NostrEvent[] = [];
+
+ const TestWrapper = createTestWrapper();
+
+ render(
+
+ { receivedEvents = events; }} />
+
+ );
+
+ // Wait for the query to complete (metadata might be slower)
+ await waitFor(() => {
+ expect(receivedEvents.length).toBeGreaterThan(0);
+ }, { timeout: 20000 });
+
+ // Verify we got valid metadata
+ expect(receivedEvents).toBeDefined();
+ expect(Array.isArray(receivedEvents)).toBe(true);
+
+ const event = receivedEvents[0];
+ expect(event.kind).toBe(0);
+ expect(event.pubkey).toBe(TEST_AUTHOR_PUBKEY);
+ expect(typeof event.content).toBe('string');
+
+ // Parse and validate the profile metadata
+ const profile = JSON.parse(event.content);
+ expect(typeof profile).toBe('object');
+ // Common profile fields (optional)
+ if (profile.name) expect(typeof profile.name).toBe('string');
+ if (profile.about) expect(typeof profile.about).toBe('string');
+ if (profile.picture) expect(typeof profile.picture).toBe('string');
+ }, 25000);
+
+ it('should handle stream queries from real relays', async () => {
+ const TestWrapper = createTestWrapper();
+ const streamEvents: NostrEvent[] = [];
let receivedEOSE = false;
- const stream = pool.req([
- {
- kinds: [1],
- limit: 2,
- }
- ], {
- signal: AbortSignal.timeout(10000),
- });
-
- for await (const msg of stream) {
- if (msg[0] === 'EVENT') {
- events.push(msg[2]);
- } else if (msg[0] === 'EOSE') {
- receivedEOSE = true;
- break;
- } else if (msg[0] === 'CLOSED') {
- break;
- }
+ function TestStream() {
+ const { nostr } = useNostr();
- // Break early if we get enough events
- if (events.length >= 2) {
- break;
- }
- }
+ React.useEffect(() => {
+ const runStream = async () => {
+ try {
+ const stream = nostr.req([
+ {
+ kinds: [1],
+ limit: 2,
+ }
+ ], {
+ signal: AbortSignal.timeout(15000),
+ });
- expect(receivedEOSE).toBe(true);
+ for await (const msg of stream) {
+ if (msg[0] === 'EVENT') {
+ streamEvents.push(msg[2]);
+ } else if (msg[0] === 'EOSE') {
+ receivedEOSE = true;
+ break;
+ } else if (msg[0] === 'CLOSED') {
+ break;
+ }
+
+ // Break early if we get enough events
+ if (streamEvents.length >= 2) {
+ break;
+ }
+ }
+ } catch (error) {
+ console.error('Stream failed:', error);
+ }
+ };
+
+ runStream();
+ }, [nostr]);
+
+ return Testing Stream
;
+ }
- if (events.length > 0) {
- const event = events[0];
+ render(
+
+
+
+ );
+
+ // Wait for stream to complete
+ await waitFor(() => {
+ expect(receivedEOSE).toBe(true);
+ }, { timeout: 20000 });
+
+ if (streamEvents.length > 0) {
+ const event = streamEvents[0];
expect(event).toHaveProperty('id');
expect(event).toHaveProperty('kind');
expect(event.kind).toBe(1);
}
- }, 15000);
-
- it('should publish event to real relays', async () => {
- // Generate a test event (note: this won't actually publish since sig is fake)
- const testEvent = createTestEvent();
-
- // This should not throw an error (though relay may reject due to invalid sig)
- await expect(pool.event(testEvent)).resolves.not.toThrow();
- }, 15000);
-
- it('should handle metadata queries to indexer relays', async () => {
- // Query for some profile metadata (kind 0)
- const events = await pool.query([
- {
- kinds: [0],
- limit: 2,
- }
- ], {
- signal: AbortSignal.timeout(10000),
- });
-
- expect(events).toBeDefined();
- expect(Array.isArray(events)).toBe(true);
-
- if (events.length > 0) {
- const event = events[0];
- expect(event.kind).toBe(0);
- expect(typeof event.content).toBe('string');
-
- // Try to parse the content as JSON (kind 0 should be JSON)
- try {
- const profile = JSON.parse(event.content);
- expect(typeof profile).toBe('object');
- } catch {
- // Some kind 0 events might have invalid JSON, that's ok
- }
- }
- }, 15000);
-
- it('should handle connection errors gracefully', async () => {
- // Create a router with an invalid relay URL
- const badRouter = new Router({
- getUserPubkey: () => null,
- getGroupRelays: () => [],
- getCommunityRelays: () => [],
- getPubkeyRelays: () => [],
- getStaticRelays: () => ['wss://invalid.relay.that.does.not.exist'],
- getIndexerRelays: () => ['wss://invalid.relay.that.does.not.exist'],
- getSearchRelays: () => ['wss://invalid.relay.that.does.not.exist'],
- getRelayQuality: () => 0.1,
- getRedundancy: () => 1,
- getLimit: () => 1,
- });
-
- const badPool = new NWelshman(badRouter as unknown as import('@welshman/util').Router);
-
- // Query should not crash but may return empty results
- const events = await badPool.query([
- {
- kinds: [1],
- limit: 1,
- }
- ], {
- signal: AbortSignal.timeout(5000),
- });
-
- expect(events).toBeDefined();
- expect(Array.isArray(events)).toBe(true);
- // We expect empty results due to connection failure
- expect(events.length).toBe(0);
- }, 10000);
-
- it('should respect query timeouts', async () => {
- const startTime = Date.now();
-
- try {
- await pool.query([
- {
- kinds: [1],
- limit: 100,
- }
- ], {
- signal: AbortSignal.timeout(1000), // Very short timeout
- });
- } catch (error) {
- // Timeout is expected, check that it happened reasonably quickly
- const elapsed = Date.now() - startTime;
- expect(elapsed).toBeLessThan(3000); // Should timeout within 3 seconds
- expect(error).toBeDefined();
- }
- });
+ }, 25000);
});
\ No newline at end of file
diff --git a/src/components/NostrProvider.simple.test.tsx b/src/components/NostrProvider.simple.test.tsx
new file mode 100644
index 0000000..046d934
--- /dev/null
+++ b/src/components/NostrProvider.simple.test.tsx
@@ -0,0 +1,46 @@
+import React from 'react';
+import { describe, it, expect } from 'vitest';
+import { render } from '@testing-library/react';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import NostrProvider from './NostrProvider';
+import { AppProvider } from '@/components/AppProvider';
+import { AppConfig } from '@/contexts/AppContext';
+
+// Simple test to validate our NostrProvider can be instantiated
+const testConfig: AppConfig = {
+ theme: "light",
+ relayUrl: "wss://relay.damus.io",
+};
+
+const presetRelays = [
+ { url: 'wss://relay.nostr.band', name: 'Nostr.Band' },
+];
+
+describe('NostrProvider Simple Validation', () => {
+ it('should render without crashing', () => {
+ const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: { retry: false },
+ mutations: { retry: false },
+ },
+ });
+
+ const TestChild = () => Test
;
+
+ const { getByTestId } = render(
+
+
+
+
+
+
+
+ );
+
+ expect(getByTestId('test-child')).toBeInTheDocument();
+ });
+});
\ No newline at end of file
diff --git a/src/components/NostrProvider.tsx b/src/components/NostrProvider.tsx
index a15ef3c..bdd95df 100644
--- a/src/components/NostrProvider.tsx
+++ b/src/components/NostrProvider.tsx
@@ -3,13 +3,18 @@ import { NWelshman } from '@nostrify/welshman';
import { NostrContext } from '@nostrify/react';
import { useQueryClient } from '@tanstack/react-query';
import { useAppContext } from '@/hooks/useAppContext';
+import type { NPool } from '@nostrify/nostrify';
-// Type definitions to match what NWelshman expects
+// Import RelayMode from welshman util (Router is not exported from current version)
+import { RelayMode } from '@welshman/util';
+
+// Create a type-compatible Router class that matches the expected interface
+// This is based on the Router interface from @welshman/util v0.0.2
interface RouterOptions {
getUserPubkey: () => string | null;
getGroupRelays: (address: string) => string[];
getCommunityRelays: (address: string) => string[];
- getPubkeyRelays: (pubkey: string, mode?: unknown) => string[];
+ getPubkeyRelays: (pubkey: string, mode?: RelayMode) => string[];
getStaticRelays: () => string[];
getIndexerRelays: () => string[];
getSearchRelays: () => string[];
@@ -18,9 +23,201 @@ interface RouterOptions {
getLimit: () => number;
}
-// Mock Router class that satisfies NWelshman requirements
+interface RouterScenario {
+ getUrls(): string[];
+ getSelections(): Array<{ relay: string; values: string[] }>;
+ policy(fallbackPolicy: (count: number, limit: number) => number): RouterScenario;
+}
+
class Router {
- constructor(public options: RouterOptions) {}
+ // Make options public so NWelshman can access it
+ public readonly options: RouterOptions;
+
+ // Fallback policy function that NWelshman expects
+ public readonly addMinimalFallbacks = (count: number, limit: number): number => {
+ return Math.min(count + 1, limit);
+ };
+
+ constructor(options: RouterOptions) {
+ this.options = options;
+ }
+
+ // Router methods that NWelshman expects
+ User(): RouterScenario {
+ const relays = this.options.getStaticRelays();
+ return this.scenario(relays.map(relay => ({
+ value: 'user',
+ relays: [relay]
+ })));
+ }
+
+ FromPubkeys(pubkeys: string[]): RouterScenario {
+ const relaySelections: Array<{ relay: string; values: string[] }> = [];
+
+ // For each pubkey, get their relays and create selections
+ for (const pubkey of pubkeys) {
+ const relays = this.options.getPubkeyRelays(pubkey);
+ for (const relay of relays) {
+ const existing = relaySelections.find(s => s.relay === relay);
+ if (existing) {
+ existing.values.push(pubkey);
+ } else {
+ relaySelections.push({ relay, values: [pubkey] });
+ }
+ }
+ }
+
+ return this.scenarioFromSelections(relaySelections);
+ }
+
+ WithinMultipleContexts(addresses: string[]): RouterScenario {
+ // For contexts, use community/group relays or fall back to static relays
+ const relaySelections: Array<{ relay: string; values: string[] }> = [];
+
+ for (const address of addresses) {
+ const relays = this.options.getCommunityRelays(address) ||
+ this.options.getGroupRelays(address) ||
+ this.options.getStaticRelays();
+
+ for (const relay of relays) {
+ const existing = relaySelections.find(s => s.relay === relay);
+ if (existing) {
+ existing.values.push(address);
+ } else {
+ relaySelections.push({ relay, values: [address] });
+ }
+ }
+ }
+
+ return this.scenarioFromSelections(relaySelections);
+ }
+
+ PublishEvent(_event: unknown): RouterScenario {
+ return this.scenario(this.options.getStaticRelays().map(relay => ({
+ value: 'publish',
+ relays: [relay]
+ })));
+ }
+
+ product(values: string[], relays: string[]): RouterScenario {
+ const selections: Array<{ relay: string; values: string[] }> = [];
+
+ for (const relay of relays) {
+ selections.push({ relay, values: [...values] });
+ }
+
+ return this.scenarioFromSelections(selections);
+ }
+
+ merge(scenarios: RouterScenario[]): RouterScenario {
+ const allSelections: Array<{ relay: string; values: string[] }> = [];
+
+ for (const scenario of scenarios) {
+ allSelections.push(...scenario.getSelections());
+ }
+
+ // Merge selections by relay
+ const mergedSelections: Array<{ relay: string; values: string[] }> = [];
+ for (const selection of allSelections) {
+ const existing = mergedSelections.find(s => s.relay === selection.relay);
+ if (existing) {
+ existing.values.push(...selection.values);
+ } else {
+ mergedSelections.push({
+ relay: selection.relay,
+ values: [...selection.values]
+ });
+ }
+ }
+
+ return this.scenarioFromSelections(mergedSelections);
+ }
+
+ private scenario(valueRelays: Array<{ value: string; relays: string[] }>): RouterScenario {
+ return new RouterScenarioImpl(this, valueRelays);
+ }
+
+ private scenarioFromSelections(relaySelections: Array<{ relay: string; values: string[] }>): RouterScenario {
+ // Convert relay selections to value relays format
+ const valueRelays: Array<{ value: string; relays: string[] }> = [];
+
+ for (const selection of relaySelections) {
+ for (const value of selection.values) {
+ const existing = valueRelays.find(vr => vr.value === value);
+ if (existing) {
+ existing.relays.push(selection.relay);
+ } else {
+ valueRelays.push({ value, relays: [selection.relay] });
+ }
+ }
+ }
+
+ return new RouterScenarioImpl(this, valueRelays);
+ }
+}
+
+class RouterScenarioImpl implements RouterScenario {
+ constructor(
+ private router: Router,
+ private valueRelays: Array<{ value: string; relays: string[] }>
+ ) {}
+
+ getUrls(): string[] {
+ const allRelays = new Set();
+ for (const vr of this.valueRelays) {
+ vr.relays.forEach(relay => allRelays.add(relay));
+ }
+ return Array.from(allRelays);
+ }
+
+ getSelections(): Array<{ relay: string; values: string[] }> {
+ const selections: Array<{ relay: string; values: string[] }> = [];
+
+ for (const vr of this.valueRelays) {
+ for (const relay of vr.relays) {
+ const existing = selections.find(s => s.relay === relay);
+ if (existing) {
+ existing.values.push(vr.value);
+ } else {
+ selections.push({ relay, values: [vr.value] });
+ }
+ }
+ }
+
+ return selections;
+ }
+
+ policy(fallbackPolicy: (count: number, limit: number) => number): RouterScenario {
+ // Apply fallback policy to limit number of relays
+ const limit = this.router.options.getLimit();
+ const redundancy = this.router.options.getRedundancy();
+ const maxRelays = fallbackPolicy(redundancy, limit);
+
+ // Sort relays by quality and take the top ones
+ const relayQuality = new Map();
+ const allRelays = new Set();
+
+ for (const vr of this.valueRelays) {
+ vr.relays.forEach(relay => {
+ allRelays.add(relay);
+ if (!relayQuality.has(relay)) {
+ relayQuality.set(relay, this.router.options.getRelayQuality(relay));
+ }
+ });
+ }
+
+ const sortedRelays = Array.from(allRelays)
+ .sort((a, b) => (relayQuality.get(b) || 0) - (relayQuality.get(a) || 0))
+ .slice(0, maxRelays);
+
+ // Filter value relays to only include top relays
+ const filteredValueRelays = this.valueRelays.map(vr => ({
+ value: vr.value,
+ relays: vr.relays.filter(relay => sortedRelays.includes(relay))
+ })).filter(vr => vr.relays.length > 0);
+
+ return new RouterScenarioImpl(this.router, filteredValueRelays);
+ }
}
interface NostrProviderProps {
@@ -68,7 +265,7 @@ const NostrProvider: React.FC = (props) => {
getCommunityRelays: (_address: string) => [],
// Get relays for a specific pubkey - for now, use configured relay
- getPubkeyRelays: (_pubkey: string, _mode?: unknown) => {
+ getPubkeyRelays: (_pubkey: string, _mode?: RelayMode) => {
return [relayUrl.current];
},
@@ -152,12 +349,16 @@ const NostrProvider: React.FC = (props) => {
}, [relayUrl, presetRelays]);
// Create NWelshman pool with the router
+ // NWelshman implements NRelay but NostrContext expects NPool interface
+ // We use type assertions to bridge the gap between the two type systems
const pool = useMemo(() => {
- return new NWelshman(router as any);
+ // Cast through unknown to avoid TypeScript's structural type checking
+ // This is safe because NWelshman implements the same query/event methods as NPool
+ return new NWelshman(router as unknown as ConstructorParameters[0]) as unknown as NPool;
}, [router]);
return (
-
+
{children}
);
diff --git a/src/main.tsx b/src/main.tsx
index e263904..4510862 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -7,7 +7,7 @@ import { ErrorBoundary } from '@/components/ErrorBoundary';
import App from './App.tsx';
import './index.css';
-// FIXME: a custom font should be used. Eg:
+// Custom font can be added. Example:
// import '@fontsource-variable/';
createRoot(document.getElementById("root")!).render(
diff --git a/src/pages/Index.tsx b/src/pages/Index.tsx
index 4427de5..1ac6c52 100644
--- a/src/pages/Index.tsx
+++ b/src/pages/Index.tsx
@@ -1,6 +1,6 @@
import { useSeoMeta } from '@unhead/react';
-// FIXME: Update this page (the content is just a fallback if you fail to update the page)
+// This is the default homepage - customize as needed
const Index = () => {
useSeoMeta({