Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ class BroadcastState {

/// [networkController] used by relay manger to write responses
StreamController<RelayBroadcastResponse> networkController =
StreamController<RelayBroadcastResponse>();
StreamController<RelayBroadcastResponse>.broadcast();

/// stream of state updates \
/// updates are sent when a relay responds, the whole state is sent \
Expand Down
19 changes: 19 additions & 0 deletions packages/ndk/lib/domain_layer/usecases/broadcast/broadcast.dart
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,25 @@ class Broadcast {
// register broadcast state
_globalState.inFlightBroadcasts[nostrEvent.id] = broadcastState;

// Listen for broadcast responses to update cache sources
broadcastState.networkController.stream.listen((response) async {
if (response.broadcastSuccessful) {
final currentEvent = broadcastState.event;
if (currentEvent != null) {
final cachedEvent = await _cacheManager.loadEvent(currentEvent.id);
if (cachedEvent != null) {
final updatedSources = {
...cachedEvent.sources,
response.relayUrl
}.toList();
final updatedEvent =
cachedEvent.copyWith(sources: updatedSources);
await _cacheManager.saveEvent(updatedEvent);
}
}
}
});
Comment on lines +74 to +91

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "broadcast.dart" | grep -E "domain_layer|usecases"

Repository: relaystr/ndk

Length of output: 121


🏁 Script executed:

find . -type f -name "relay_manager.dart" | grep -E "domain_layer|usecases"

Repository: relaystr/ndk

Length of output: 115


🏁 Script executed:

cat -n packages/ndk/lib/domain_layer/usecases/broadcast/broadcast.dart | sed -n '50,110p'

Repository: relaystr/ndk

Length of output: 2890


🏁 Script executed:

cat -n packages/ndk/lib/domain_layer/usecases/relay_manager.dart | sed -n '490,530p'

Repository: relaystr/ndk

Length of output: 2053


🏁 Script executed:

cat -n packages/ndk/lib/domain_layer/usecases/broadcast/broadcast.dart | sed -n '1,20p'

Repository: relaystr/ndk

Length of output: 844


🏁 Script executed:

wc -l packages/ndk/lib/domain_layer/usecases/broadcast/broadcast.dart

Repository: relaystr/ndk

Length of output: 123


🏁 Script executed:

cat -n packages/ndk/lib/domain_layer/usecases/broadcast/broadcast.dart | sed -n '93,100p'

Repository: relaystr/ndk

Length of output: 333


🏁 Script executed:

cat -n packages/ndk/lib/domain_layer/usecases/relay_manager.dart | sed -n '500,530p'

Repository: relaystr/ndk

Length of output: 1503


🏁 Script executed:

cat -n packages/ndk/lib/domain_layer/entities/broadcast_state.dart | head -30

Repository: relaystr/ndk

Length of output: 1066


🏁 Script executed:

rg -A 3 "class BroadcastState" packages/ndk/lib/domain_layer/entities/broadcast_state.dart

Repository: relaystr/ndk

Length of output: 219


Use the in-memory broadcast state to avoid losing relay sources to cache read-modify-write races.

Lines 75–91 listen to broadcast responses and load the cached event once per relay, merge only that relay's URL, and save. If two relays respond before the first cache write completes, both listeners read the same stale cached snapshot, and the second save loses the first relay's source. Since relay_manager.dart (lines 504–517) already maintains broadcastState.event with the monotonic union of all successful relays before emitting the response, merge the cached sources with broadcastState.event.sources instead of patching the cache one relay at a time.

Suggested fix
-    broadcastState.networkController.stream.listen((response) async {
-      if (response.broadcastSuccessful) {
-        final currentEvent = broadcastState.event;
-        if (currentEvent != null) {
-          final cachedEvent = await _cacheManager.loadEvent(currentEvent.id);
-          if (cachedEvent != null) {
-            final updatedSources = {
-              ...cachedEvent.sources,
-              response.relayUrl
-            }.toList();
-            final updatedEvent =
-                cachedEvent.copyWith(sources: updatedSources);
-            await _cacheManager.saveEvent(updatedEvent);
-          }
-        }
-      }
-    });
+    broadcastState.networkController.stream.listen((response) async {
+      if (!mySaveToCache || !response.broadcastSuccessful) {
+        return;
+      }
+
+      final eventId = broadcastState.event?.id;
+      if (eventId == null) {
+        return;
+      }
+
+      final cachedEvent = await _cacheManager.loadEvent(eventId);
+      final latestEvent = broadcastState.event;
+      if (latestEvent == null) {
+        return;
+      }
+
+      final updatedSources = {
+        ...?cachedEvent?.sources,
+        ...latestEvent.sources,
+      }.toList();
+
+      await _cacheManager.saveEvent(
+        latestEvent.copyWith(sources: updatedSources),
+      );
+    });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ndk/lib/domain_layer/usecases/broadcast/broadcast.dart` around lines
74 - 91, The listener on broadcastState.networkController.stream currently does
a read-modify-write using the cached event per-relay, causing races; instead,
when response.broadcastSuccessful is true use the in-memory
broadcastState.event.sources (the monotonic union maintained by relay_manager)
as the source of truth: load the cached event via
_cacheManager.loadEvent(currentEvent.id), merge its sources with
broadcastState.event.sources (ensuring uniqueness), create an updatedEvent via
cachedEvent.copyWith(sources: mergedSources) and call
_cacheManager.saveEvent(updatedEvent); keep checks for currentEvent and
cachedEvent but stop merging only response.relayUrl so the cache reflects
broadcastState.event.sources.


// save event to cache if enabled
if (mySaveToCache) {
_cacheManager.saveEvent(nostrEvent);
Expand Down
18 changes: 17 additions & 1 deletion packages/ndk/lib/domain_layer/usecases/relay_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -501,6 +501,22 @@ class RelayManager<T> {
if (globalState.inFlightBroadcasts[eventId] != null &&
!globalState
.inFlightBroadcasts[eventId]!.networkController.isClosed) {
// Update cache with source if broadcast was successful
Comment thread
nogringo marked this conversation as resolved.
if (success) {
final broadcastState = globalState.inFlightBroadcasts[eventId];
final event = broadcastState?.event;
if (event != null) {
// Merge existing sources with new relay URL, avoiding duplicates
final updatedSources = {
...event.sources,
relayConnectivity.url
}.toList();
final updatedEvent = event.copyWith(sources: updatedSources);
// Update the event in broadcast state
broadcastState!.event = updatedEvent;
}
}

globalState.inFlightBroadcasts[eventId]?.networkController.add(
RelayBroadcastResponse(
relayUrl: relayConnectivity.url,
Expand Down Expand Up @@ -659,7 +675,7 @@ class RelayManager<T> {
}

final eventWithSources =
event.copyWith(sources: [...event.sources, connectivity.url]);
event.copyWith(sources: {...event.sources, connectivity.url}.toList());

if (state.networkController.isClosed) {
// this might happen because relays even after we send a CLOSE subscription.id, they'll still send more events
Expand Down
33 changes: 28 additions & 5 deletions packages/ndk/lib/domain_layer/usecases/requests/requests.dart
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,7 @@ class Requests {
trackingSet: state.returnedIds,
outController: state.controller,
eventOutFilters: _eventOutFilters,
cacheManager: _cacheWrite.cacheManager,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Honor cacheWrite at Line 278.

packages/ndk/lib/domain_layer/usecases/stream_response_cleaner/stream_response_cleaner.dart Lines 86-95 now call saveEvent(). Passing _cacheWrite.cacheManager unconditionally here means a request with cacheWrite: false can still mutate cached sources when the same event arrives again.

💡 Suggested fix
-      cacheManager: _cacheWrite.cacheManager,
+      cacheManager: request.cacheWrite ? _cacheWrite.cacheManager : null,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ndk/lib/domain_layer/usecases/requests/requests.dart` at line 278,
The code unconditionally passes _cacheWrite.cacheManager (via the cacheManager
parameter) which allows cache mutations even when cacheWrite is false; change
the call site that currently sets cacheManager: _cacheWrite.cacheManager to pass
the cache manager only when caching is enabled (e.g., cacheManager:
_cacheWrite.cacheWrite ? _cacheWrite.cacheManager : null), so that downstream
saveEvent() in stream_response_cleaner (which calls saveEvent() at lines 86-95)
will not mutate sources for requests with cacheWrite: false.

)();

// Record fetched ranges when network requests complete (EOSE received)
Expand Down Expand Up @@ -345,7 +346,7 @@ class Requests {
}) {
final requestId = '$name-paginated-${Helpers.getRandomString(10)}';
final aggregatedController = ReplaySubject<Nip01Event>();
final seenEventIds = <String>{};
final seenEvents = <String, Set<String>>{}; // event_id -> sources

Future<void> paginate() async {
final since = filter.since;
Expand Down Expand Up @@ -373,9 +374,20 @@ class Requests {
final relayState = <String, _RelayPaginationState>{};

for (final event in initialEvents) {
if (!seenEventIds.contains(event.id)) {
seenEventIds.add(event.id);
final existingSources = seenEvents[event.id];
if (existingSources == null) {
// First time seeing this event
seenEvents[event.id] = event.sources.toSet();
aggregatedController.add(event);
} else {
// Merge sources if this event has new sources
if (event.sources.isNotEmpty) {
final newSources = existingSources..addAll(event.sources);
if (newSources.length > (seenEvents[event.id]?.length ?? 0)) {
seenEvents[event.id] = newSources;
aggregatedController.add(event.copyWith(sources: newSources.toList()));
}
}
Comment on lines +377 to +390

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

fd -t f -i 'requests.dart' | head -5

Repository: relaystr/ndk

Length of output: 191


🏁 Script executed:

wc -l packages/ndk/lib/domain_layer/usecases/requests/requests.dart

Repository: relaystr/ndk

Length of output: 121


🏁 Script executed:

sed -n '370,400p' packages/ndk/lib/domain_layer/usecases/requests/requests.dart | cat -n

Repository: relaystr/ndk

Length of output: 1470


🏁 Script executed:

sed -n '460,485p' packages/ndk/lib/domain_layer/usecases/requests/requests.dart | cat -n

Repository: relaystr/ndk

Length of output: 1413


🏁 Script executed:

# Let me verify the exact context around these lines and understand the data flow
sed -n '375,405p' packages/ndk/lib/domain_layer/usecases/requests/requests.dart | cat -n

Repository: relaystr/ndk

Length of output: 1385


🏁 Script executed:

cat > /tmp/test_dart.txt << 'EOF'
In Dart:
- The cascade operator (..) mutates the object and returns self
- existingSources is a reference to the Set in seenEvents
- existingSources..addAll(event.sources) mutates that Set and returns it
- So seenEvents[event.id] points to the same mutated Set
- Comparison happens after mutation, so both sides have same length
- This means the condition is always false when sources are added
EOF
cat /tmp/test_dart.txt

Repository: relaystr/ndk

Length of output: 450


🏁 Script executed:

# Let me check the actual line numbers in the original review to map them correctly
sed -n '377,390p' packages/ndk/lib/domain_layer/usecases/requests/requests.dart | cat -n

Repository: relaystr/ndk

Length of output: 810


🏁 Script executed:

sed -n '468,480p' packages/ndk/lib/domain_layer/usecases/requests/requests.dart | cat -n

Repository: relaystr/ndk

Length of output: 843


🏁 Script executed:

cat << 'EOF'
Dart Set behavior with cascade operator:

Given:
  final existingSources = seenEvents[event.id];  // reference to Set in map

When executing:
  final newSources = existingSources..addAll(event.sources);

The cascade operator (..) on existingSources:
1. Calls addAll() which MUTATES existingSources in-place
2. Returns the mutated existingSources (same object reference)
3. Assigns it to newSources

Result: newSources and existingSources reference the SAME Set object

When checking:
  if (newSources.length > (seenEvents[event.id]?.length ?? 0))

Both sides reference the SAME mutated Set, so:
- newSources.length == seenEvents[event.id].length (always)
- Condition is always FALSE
- Event is never emitted with merged sources

The suggested fix {...existingSources, ...event.sources} creates a NEW Set
via spread syntax, avoiding mutation and allowing proper comparison.
EOF

Repository: relaystr/ndk

Length of output: 928


Clone the tracked set before comparing sizes.

existingSources..addAll(event.sources) mutates the same Set stored in seenEvents, causing the length comparison on line 386 to always be false since both sides reference the identical mutated object. This prevents merged events from being emitted and blocks relay discovery from expanding when a new relay appears through duplicates.

Replace with final newSources = {...existingSources, ...event.sources}; to create a new Set, and simplify the comparison to if (newSources.length > existingSources.length). This pattern appears identically at both locations (lines 377-390 and 468-480).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ndk/lib/domain_layer/usecases/requests/requests.dart` around lines
377 - 390, The code mutates the stored Set by doing
existingSources..addAll(event.sources), so the size comparison always fails;
instead create a new Set clone when merging sources (e.g., newSources =
{...existingSources, ...event.sources}) and compare newSources.length to
existingSources.length, and only update seenEvents[event.id] and call
aggregatedController.add(event.copyWith(...)) when newSources is larger; apply
this change for both occurrences handling seenEvents, existingSources,
aggregatedController, and event.copyWith in the file.

}

// Track oldest timestamp per relay
Expand Down Expand Up @@ -453,9 +465,20 @@ class Requests {

int? oldestTimestamp;
for (final event in pageEvents) {
if (!seenEventIds.contains(event.id)) {
seenEventIds.add(event.id);
final existingSources = seenEvents[event.id];
if (existingSources == null) {
// First time seeing this event
seenEvents[event.id] = event.sources.toSet();
aggregatedController.add(event);
} else {
// Merge sources if this event has new sources
if (event.sources.isNotEmpty) {
final newSources = existingSources..addAll(event.sources);
if (newSources.length > (seenEvents[event.id]?.length ?? 0)) {
seenEvents[event.id] = newSources;
aggregatedController.add(event.copyWith(sources: newSources.toList()));
}
}
}
// Track oldest timestamp for this relay
if (oldestTimestamp == null || event.createdAt < oldestTimestamp) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@ import 'dart:async';
import '../../../shared/logger/logger.dart';
import '../../entities/event_filter.dart';
import '../../entities/nip_01_event.dart';
import '../../repositories/cache_manager.dart';

/// given a stream with Nip01 events it tracks the id and adds the one to the provided stream controller \
/// tracking of the happens in the tracking list
class StreamResponseCleaner {
final Set<String> _trackingSet;
final Map<String, Set<String>> _trackingMap; // event_id -> set of sources
final List<Stream<Nip01Event>> _inputStreams;
final StreamController<Nip01Event> _outController;
final List<EventFilter> _eventOutFilters;
final CacheManager? _cacheManager;

Comment on lines +14 to 15

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

fd -t f "stream_response_cleaner.dart"

Repository: relaystr/ndk

Length of output: 147


🏁 Script executed:

fd -t f "requests.dart" packages/ndk/lib/domain_layer/usecases/

Repository: relaystr/ndk

Length of output: 117


🏁 Script executed:

cat -n packages/ndk/lib/domain_layer/usecases/stream_response_cleaner/stream_response_cleaner.dart

Repository: relaystr/ndk

Length of output: 4389


🏁 Script executed:

sed -n '260,285p' packages/ndk/lib/domain_layer/usecases/requests/requests.dart

Repository: relaystr/ndk

Length of output: 756


🏁 Script executed:

# Also check the CacheManager interface to understand the contract
fd -t f "cache_manager.dart" packages/ndk/lib

Repository: relaystr/ndk

Length of output: 195


🏁 Script executed:

cat -n packages/ndk/lib/domain_layer/repositories/cache_manager.dart

Repository: relaystr/ndk

Length of output: 8290


Queue cache writes per event ID to ensure sources never shrink.

Line 61 fires an unordered async cache update that can silently drop on cache miss (line 91) or cause earlier writes to complete last and shrink stored sources. In packages/ndk/lib/domain_layer/usecases/requests/requests.dart lines 264-279, the initial cache write and this merge run independently off the same verified stream, enabling out-of-order completion. Add a per-event-ID queue to chain updates sequentially and save the merged event snapshot (sources from both cache and current event).

Suggested fix
   final List<EventFilter> _eventOutFilters;
   final CacheManager? _cacheManager;
+  final Map<String, Future<void>> _pendingCacheUpdates = {};
@@
-            final mergedEvent = event.copyWith(sources: newSources.toList());
-            _outController.add(mergedEvent);
-            // Update cache with merged sources
-            _updateCacheSources(event.id, newSources);
+            final mergedEvent = event.copyWith(sources: newSources.toList());
+            _outController.add(mergedEvent);
+            _updateCacheSources(mergedEvent);
@@
-  void _updateCacheSources(String eventId, Set<String> sources) {
-    if (_cacheManager == null) return;
-    
-    // Load existing event from cache and update sources
-    _cacheManager.loadEvent(eventId).then((cachedEvent) {
-      if (cachedEvent != null) {
-        final updatedEvent = cachedEvent.copyWith(sources: sources.toList());
-        _cacheManager.saveEvent(updatedEvent);
-      }
-    });
+  Future<void> _updateCacheSources(Nip01Event event) {
+    if (_cacheManager == null) return Future.value();
+
+    final previous = _pendingCacheUpdates[event.id] ?? Future.value();
+    final queued = previous.then((_) async {
+      final cachedEvent = await _cacheManager!.loadEvent(event.id);
+      final mergedSources = {
+        ...?cachedEvent?.sources,
+        ...event.sources,
+      }.toList();
+
+      await _cacheManager!.saveEvent(
+        (cachedEvent ?? event).copyWith(sources: mergedSources),
+      );
+    });
+
+    _pendingCacheUpdates[event.id] = queued;
+    return queued;
+  }

Also applies to: 58-61, 85-95

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/ndk/lib/domain_layer/usecases/stream_response_cleaner/stream_response_cleaner.dart`
around lines 14 - 15, The async cache merge in StreamResponseCleaner currently
fires unordered updates using _cacheManager which can race with the initial
cache write in the Requests use-case; implement a per-event-ID queue (map of
eventId -> Future) inside StreamResponseCleaner to chain cache update operations
so each event's writes run sequentially, on each queued turn read the current
cache entry, merge its sources with the incoming event's sources to produce a
merged snapshot, then write that merged snapshot back via _cacheManager; ensure
the queue entry is replaced with the next Future only after the write completes
or errors so subsequent updates for the same eventId never run concurrently or
lose sources.

int get _numStreams => _inputStreams.length;

Expand All @@ -18,16 +20,19 @@ class StreamResponseCleaner {
/// - [trackingSet] a set of ids that are already returned \
/// - [inputStreams] a list of streams that are be listened to \
/// - [outController] the controller that is used to add the events to \
/// - [cacheManager] optional cache manager to persist event sources \

StreamResponseCleaner({
required Set<String> trackingSet,
required List<Stream<Nip01Event>> inputStreams,
required StreamController<Nip01Event> outController,
required List<EventFilter> eventOutFilters,
}) : _trackingSet = trackingSet,
CacheManager? cacheManager,
}) : _trackingMap = {for (final id in trackingSet) id: {}},
_outController = outController,
_inputStreams = inputStreams,
_eventOutFilters = eventOutFilters;
_eventOutFilters = eventOutFilters,
_cacheManager = cacheManager;

void call() {
for (final stream in _inputStreams) {
Expand All @@ -37,16 +42,30 @@ class StreamResponseCleaner {

void _addStreamListener(Stream<Nip01Event> stream) {
stream.listen((event) {
// check if event id is in the set
if (_trackingSet.contains(event.id)) {
if (_outController.isClosed) {
return;
}

if (_outController.isClosed) {
// check if event id is already seen
final existingSources = _trackingMap[event.id];
Comment thread
nogringo marked this conversation as resolved.
if (existingSources != null) {
// Event already seen - merge sources if this event has new sources
if (event.sources.isNotEmpty) {
final newSources = Set<String>.from(existingSources)..addAll(event.sources);
// Only emit if we have new sources to add
if (newSources.length > existingSources.length) {
_trackingMap[event.id] = newSources;
final mergedEvent = event.copyWith(sources: newSources.toList());
_outController.add(mergedEvent);
// Update cache with merged sources
_updateCacheSources(event.id, newSources);
}
}
return;
}

_trackingSet.add(event.id);
// First time seeing this event
_trackingMap[event.id] = event.sources.toSet();

// check against filters
for (final filter in _eventOutFilters) {
Expand All @@ -63,6 +82,19 @@ class StreamResponseCleaner {
});
}

/// Updates the cache with merged sources for an event
void _updateCacheSources(String eventId, Set<String> sources) {
if (_cacheManager == null) return;

// Load existing event from cache and update sources
_cacheManager.loadEvent(eventId).then((cachedEvent) {
if (cachedEvent != null) {
final updatedEvent = cachedEvent.copyWith(sources: sources.toList());
_cacheManager.saveEvent(updatedEvent);
}
});
}

/// used to wait on all streams
Future<void> _canClose() async {
_closedStreams++;
Expand Down
1 change: 0 additions & 1 deletion packages/ndk/lib/presentation_layer/init.dart
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,6 @@ class Initialization {
eagerAuth: _ndkConfig.eagerAuth,
authCallbackTimeout: _ndkConfig.authCallbackTimeout,
);

engine = JitEngine(
cache: _ndkConfig.cache,
ignoreRelays: _ndkConfig.ignoreRelays,
Expand Down
12 changes: 12 additions & 0 deletions packages/ndk/test/mocks/mock_relay.dart
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ class MockRelay {
bool sendMalformedEvents;
String? customWelcomeMessage;
int? maxEventsPerRequest;
String? bannedWord;

// NIP-46 Remote Signer Support
static const int kNip46Kind = BunkerRequest.kKind;
Expand All @@ -58,6 +59,7 @@ class MockRelay {
this.sendMalformedEvents = false,
this.customWelcomeMessage,
this.maxEventsPerRequest,
this.bannedWord,
int? explicitPort,
}) : _nip65s = nip65s {
if (explicitPort != null) {
Expand Down Expand Up @@ -147,6 +149,16 @@ class MockRelay {
if (eventJson[0] == "EVENT") {
Nip01Event newEvent = Nip01EventModel.fromJson(eventJson[1]);
if (verify(newEvent.pubKey, newEvent.id, newEvent.sig!)) {
// Check if event contains banned word
if (bannedWord != null && newEvent.content.contains(bannedWord!)) {
webSocket.add(jsonEncode([
"OK",
newEvent.id,
false,
"blocked: content contains banned word"
]));
return;
}
// Check auth for events if required (any authenticated user is OK)
if (requireAuthForEvents && authenticatedPubkeys.isEmpty) {
webSocket.add(jsonEncode([
Expand Down
43 changes: 43 additions & 0 deletions packages/ndk/test/usecases/broadcast_sources_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import 'package:ndk/ndk.dart';
import 'package:ndk/shared/nips/nip01/bip340.dart';
import 'package:test/test.dart';

import '../mocks/mock_event_verifier.dart';
import '../mocks/mock_relay.dart';

void main() async {
test("broadcast should update source", () async {
final relay = MockRelay(name: "relay");

await relay.startServer();
addTearDown(() => relay.stopServer());

final ndk = Ndk(NdkConfig(
eventVerifier: MockEventVerifier(),
cache: MemCacheManager(),
bootstrapRelays: [relay.url],
));
addTearDown(() => ndk.destroy());

final keypair = Bip340.generatePrivateKey();
final signer = Bip340EventSigner(
privateKey: keypair.privateKey,
publicKey: keypair.publicKey,
);
ndk.accounts.loginExternalSigner(signer: signer);

final event = Nip01Event(
pubKey: keypair.publicKey,
kind: 1,
tags: [],
content: "content",
);

await ndk.broadcast.broadcast(nostrEvent: event).broadcastDoneFuture;

final localEvent = await ndk.config.cache.loadEvent(event.id);

expect(localEvent, isNotNull);
expect(localEvent!.sources, isNotEmpty);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import 'package:ndk/shared/nips/nip01/bip340.dart';
import 'package:test/test.dart';
import 'package:ndk/ndk.dart';

import '../../mocks/mock_event_verifier.dart';
import '../../mocks/mock_relay.dart';

void main() async {
test("requests should update sources", () async {
final bannedWord = "cow";

final relay1 = MockRelay(name: "relay 1");
final relay2 = MockRelay(name: "relay 2");
final relay3 = MockRelay(name: "relay 3", bannedWord: bannedWord);

await relay1.startServer();
await relay2.startServer();
await relay3.startServer();

addTearDown(() async {
await relay1.stopServer();
await relay2.stopServer();
await relay3.stopServer();
});

final ndk = Ndk(NdkConfig(
eventVerifier: MockEventVerifier(),
cache: MemCacheManager(),
bootstrapRelays: [relay1.url, relay2.url, relay3.url],
));

addTearDown(() => ndk.destroy());

final keypair = Bip340.generatePrivateKey();
final signer = Bip340EventSigner(
privateKey: keypair.privateKey,
publicKey: keypair.publicKey,
);
ndk.accounts.loginExternalSigner(signer: signer);

final event = Nip01Event(
pubKey: keypair.publicKey,
kind: 1,
tags: [],
content: bannedWord,
);
await ndk.broadcast.broadcast(nostrEvent: event).broadcastDoneFuture;

await ndk.config.cache.clearAll();

final query = ndk.requests.query(filter: Filter(ids: [event.id]));
final events = await query.future;

// The last event should have all merged sources
expect(events.last.sources.length, equals(2));

final localEvent = await ndk.config.cache.loadEvent(event.id);

expect(localEvent, isNotNull);
expect(localEvent!.sources.length, equals(2));
});
}
Loading