From 5920d00bb32750067b82d412e00c1083e8741f02 Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Tue, 26 May 2026 10:55:28 +0200 Subject: [PATCH 1/3] Extract RelayPool seen-note recording Move the shared seen, count, and notification update into a note-id helper. This commit does not change behavior. Signed-off-by: Sjors Provoost --- damus/Core/Nostr/RelayPool.swift | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/damus/Core/Nostr/RelayPool.swift b/damus/Core/Nostr/RelayPool.swift index c82ce883..99101f4e 100644 --- a/damus/Core/Nostr/RelayPool.swift +++ b/damus/Core/Nostr/RelayPool.swift @@ -734,15 +734,21 @@ class RelayPool { } } + /// Records that a relay is known to have accepted or returned a note. + func record_seen(relay_id: RelayURL, note_id: NoteId) { + if seen[note_id]?.contains(relay_id) == true { + return + } + + seen[note_id, default: Set()].insert(relay_id) + counts[relay_id, default: 0] += 1 + notify(.update_stats(note_id: note_id)) + } + func record_seen(relay_id: RelayURL, event: NostrConnectionEvent) { if case .nostr_event(let ev) = event { if case .event(_, let nev) = ev { - if seen[nev.id]?.contains(relay_id) == true { - return - } - seen[nev.id, default: Set()].insert(relay_id) - counts[relay_id, default: 0] += 1 - notify(.update_stats(note_id: nev.id)) + record_seen(relay_id: relay_id, note_id: nev.id) } } } From 8c5b514057f5e243a33de862d7b91198c3bd36f1 Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Tue, 26 May 2026 10:56:06 +0200 Subject: [PATCH 2/3] Fix RelayPool tests and re-add to target Restore RelayPoolTests to damusTests after 9091cb1a removed it from the Xcode test target unintentionally. Update its add-relay checks for the current async RelayPool API. Signed-off-by: Sjors Provoost --- damus.xcodeproj/project.pbxproj | 4 +++ damusTests/RelayPoolTests.swift | 46 +++++++++++++++++++-------------- 2 files changed, 30 insertions(+), 20 deletions(-) diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj index 02a8b7d3..4e14ed7f 100644 --- a/damus.xcodeproj/project.pbxproj +++ b/damus.xcodeproj/project.pbxproj @@ -430,6 +430,7 @@ 4CE6DEF827F7A08200C66700 /* damusTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE6DEF727F7A08200C66700 /* damusTests.swift */; }; 4CE6DF0227F7A08200C66700 /* damusUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE6DF0127F7A08200C66700 /* damusUITests.swift */; }; 4CE6DF1627F8DEBF00C66700 /* RelayConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE6DF1527F8DEBF00C66700 /* RelayConnection.swift */; }; + D72A1A022F73B9F800B0010B /* RelayPoolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72A1A012F73B9F800B0010B /* RelayPoolTests.swift */; }; 4CE8794829941DA700F758CC /* RelayFilters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE8794729941DA700F758CC /* RelayFilters.swift */; }; 4CE8794E2996B16A00F758CC /* RelayToggle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE8794D2996B16A00F758CC /* RelayToggle.swift */; }; 4CE879502996B2BD00F758CC /* RelayStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE8794F2996B2BD00F758CC /* RelayStatusView.swift */; }; @@ -2838,6 +2839,7 @@ D72734272F08912F00F90677 /* DatabaseSnapshotManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseSnapshotManagerTests.swift; sourceTree = ""; }; D72734292F089EE600F90677 /* NdbMigrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NdbMigrationTests.swift; sourceTree = ""; }; D72927AC2BAB515C00F93E90 /* RelayURLTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayURLTests.swift; sourceTree = ""; }; + D72A1A012F73B9F800B0010B /* RelayPoolTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayPoolTests.swift; sourceTree = ""; }; D72A2CFF2AD9B66B002AFF62 /* EventViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventViewTests.swift; sourceTree = ""; }; D72A2D042AD9C1B5002AFF62 /* MockDamusState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockDamusState.swift; sourceTree = ""; }; D72A2D062AD9C1FB002AFF62 /* MockProfiles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockProfiles.swift; sourceTree = ""; }; @@ -4005,6 +4007,7 @@ E0E024102B7C19C20075735D /* TranslationTests.swift */, E06336A92B75832100A88E6B /* ImageMetadataTest.swift */, D7CBD1D52B8D509800BFD889 /* DamusPurpleImpendingExpirationTests.swift */, + D72A1A012F73B9F800B0010B /* RelayPoolTests.swift */, D72927AC2BAB515C00F93E90 /* RelayURLTests.swift */, D753CEA92BE9DE04001C3A5D /* MutingTests.swift */, 4C2D34402BDAF1B300F9FB44 /* NIP10Tests.swift */, @@ -6519,6 +6522,7 @@ D72A2D052AD9C1B5002AFF62 /* MockDamusState.swift in Sources */, E06336AA2B75832100A88E6B /* ImageMetadataTest.swift in Sources */, D77A96BF2F3131BE00CC3246 /* RelayHintsTests.swift in Sources */, + D72A1A022F73B9F800B0010B /* RelayPoolTests.swift in Sources */, 4C363AA02828A8DD006E126D /* LikeTests.swift in Sources */, D7A0D8752D1FE67900DCBE59 /* EditPictureControlTests.swift in Sources */, D776BE442F23301A002DA1C9 /* EntityPreloaderTests.swift in Sources */, diff --git a/damusTests/RelayPoolTests.swift b/damusTests/RelayPoolTests.swift index 9c284b7e..70f79dad 100644 --- a/damusTests/RelayPoolTests.swift +++ b/damusTests/RelayPoolTests.swift @@ -18,45 +18,51 @@ final class RelayPoolTests: XCTestCase { override func tearDownWithError() throws { } - func testAddRelay_ValidRelayURL_NoErrors() { - testAddRelays(urls: [ + @MainActor + func testAddRelay_ValidRelayURL_NoErrors() async { + await testAddRelays(urls: [ "wss://relay.damus.io" ]) } - - func testAddRelay_TwoSameURLs_ThrowsRelayAlreadyExists() { - testAddRelays(urls: [ + + @MainActor + func testAddRelay_TwoSameURLs_ThrowsRelayAlreadyExists() async { + await testAddRelays(urls: [ "wss://relay.damus.io", "wss://relay.damus.io" ], expectedError: .RelayAlreadyExists) } - - func testAddRelay_OneExtraneousSlashURL_ThrowsRelayAlreadyExists() { - testAddRelays(urls: [ + + @MainActor + func testAddRelay_OneExtraneousSlashURL_ThrowsRelayAlreadyExists() async { + await testAddRelays(urls: [ "wss://relay.damus.io", "wss://relay.damus.io/" ], expectedError: .RelayAlreadyExists) } - func testAddRelay_MultipleExtraneousSlashURL_ThrowsRelayAlreadyExists() { - testAddRelays(urls: [ + @MainActor + func testAddRelay_MultipleExtraneousSlashURL_ThrowsRelayAlreadyExists() async { + await testAddRelays(urls: [ "wss://relay.damus.io", "wss://relay.damus.io///" ], expectedError: .RelayAlreadyExists) } - - func testAddRelay_ExtraSlashURLFirst_ThrowsRelayAlreadyExists() { - testAddRelays(urls: [ + + @MainActor + func testAddRelay_ExtraSlashURLFirst_ThrowsRelayAlreadyExists() async { + await testAddRelays(urls: [ "wss://relay.damus.io///", "wss://relay.damus.io" ], expectedError: .RelayAlreadyExists) } + } -func testAddRelays(urls: [String], expectedError: RelayError? = nil) { - let ndb = Ndb()! - let relayPool = RelayPool(ndb: ndb) - let info = RelayInfo(read: true, write: true) +/// Adds relay URLs to a pool and verifies duplicate URL handling. +@MainActor +func testAddRelays(urls: [String], expectedError: RelayPool.RelayError? = nil) async { + let relayPool = RelayPool(ndb: nil) do { for relay in urls { @@ -65,14 +71,14 @@ func testAddRelays(urls: [String], expectedError: RelayError? = nil) { return } - let descriptor = RelayDescriptor(url: url, info: info) - try relayPool.add_relay(descriptor) + let descriptor = RelayPool.RelayDescriptor(url: url, info: .readWrite) + try await relayPool.add_relay(descriptor) } if expectedError != nil { XCTFail("Expected \(expectedError!) error, but no error was thrown.") } - } catch let error as RelayError where expectedError == .RelayAlreadyExists { + } catch let error as RelayPool.RelayError where expectedError == .RelayAlreadyExists { XCTAssertEqual(error, expectedError!, "Expected RelayAlreadyExists error, got \(error)") } catch { XCTFail("An unexpected error was thrown: \(error)") From 504a7cb0517ae3f922cbb10ddc97ca0b368436f1 Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Tue, 26 May 2026 11:27:47 +0200 Subject: [PATCH 3/3] Track seen relays from successful OKs Record successful relay OK acknowledgements in RelayPool.seen so posts and manual broadcasts update relay provenance. Failed OK responses remain ignored. Changelog-Fixed: Update note relay counts after successful post or broadcast relay acknowledgements. Signed-off-by: Sjors Provoost --- damus/Core/Nostr/RelayPool.swift | 2 ++ damusTests/RelayPoolTests.swift | 56 ++++++++++++++++++++++++++++++-- 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/damus/Core/Nostr/RelayPool.swift b/damus/Core/Nostr/RelayPool.swift index 99101f4e..c4df69c8 100644 --- a/damus/Core/Nostr/RelayPool.swift +++ b/damus/Core/Nostr/RelayPool.swift @@ -749,6 +749,8 @@ class RelayPool { if case .nostr_event(let ev) = event { if case .event(_, let nev) = ev { record_seen(relay_id: relay_id, note_id: nev.id) + } else if case .ok(let result) = ev, result.ok { + record_seen(relay_id: relay_id, note_id: result.event_id) } } } diff --git a/damusTests/RelayPoolTests.swift b/damusTests/RelayPoolTests.swift index 70f79dad..380ceb6d 100644 --- a/damusTests/RelayPoolTests.swift +++ b/damusTests/RelayPoolTests.swift @@ -57,6 +57,60 @@ final class RelayPoolTests: XCTestCase { ], expectedError: .RelayAlreadyExists) } + /// Creates fresh inputs for seen-recording tests. + func makeRecordSeenFixture() throws -> (pool: RelayPool, relay: RelayURL, noteID: NoteId) { + let relay = try XCTUnwrap(RelayURL("wss://relay.example.com")) + let noteID = try XCTUnwrap(NoteId(hex: String(repeating: "a", count: 64))) + return (RelayPool(ndb: nil), relay, noteID) + } + + /// Verifies successful relay OK responses update the relay provenance map. + func testRecordSeenRecordsSuccessfulOKResponses() async throws { + let (pool, relay, noteID) = try makeRecordSeenFixture() + let result = CommandResult(event_id: noteID, ok: true, msg: "") + + await pool.record_seen(relay_id: relay, event: .nostr_event(.ok(result))) + + let seenRelays = await pool.seen[noteID] + let relayCount = await pool.counts[relay] + + XCTAssertEqual(seenRelays, Set([relay])) + XCTAssertEqual(relayCount, Optional(UInt64(1))) + } + + /// Verifies duplicate relay OK responses do not increment provenance twice. + func testRecordSeenRecordsDuplicateSuccessfulOKResponsesOnce() async throws { + let (pool, relay, noteID) = try makeRecordSeenFixture() + let acceptedResult = CommandResult(event_id: noteID, ok: true, msg: "") + let duplicateResult = CommandResult( + event_id: noteID, + ok: true, + msg: "duplicate: already have this event" + ) + + await pool.record_seen(relay_id: relay, event: .nostr_event(.ok(acceptedResult))) + await pool.record_seen(relay_id: relay, event: .nostr_event(.ok(duplicateResult))) + + let seenRelays = await pool.seen[noteID] + let relayCount = await pool.counts[relay] + + XCTAssertEqual(seenRelays, Set([relay])) + XCTAssertEqual(relayCount, Optional(UInt64(1))) + } + + /// Verifies failed relay OK responses do not count as relay provenance. + func testRecordSeenIgnoresFailedOKResponses() async throws { + let (pool, relay, noteID) = try makeRecordSeenFixture() + let result = CommandResult(event_id: noteID, ok: false, msg: "blocked: test") + + await pool.record_seen(relay_id: relay, event: .nostr_event(.ok(result))) + + let seenRelays = await pool.seen[noteID] + let relayCount = await pool.counts[relay] + + XCTAssertNil(seenRelays) + XCTAssertNil(relayCount) + } } /// Adds relay URLs to a pool and verifies duplicate URL handling. @@ -84,5 +138,3 @@ func testAddRelays(urls: [String], expectedError: RelayPool.RelayError? = nil) a XCTFail("An unexpected error was thrown: \(error)") } } - -