Replace the fragile `latestRelayListEventIdHex` UserDefaults lookup in `getLatestNIP65RelayListEvent()` with a direct nostrdb query using `ndb.query(filters:maxResults:)` on the AUTHOR_KINDS index. This eliminates the stale-hex failure mode where relay add/remove operations would silently fall back to bootstrap or year-old relay lists. Add an in-memory `lastSetRelayList` cache to bridge the nostrdb async write gap — `set()` populates it, `getUserCurrentRelayList()` checks it first, and `load()` clears it once ndb has committed. Remove `latestRelayListEventIdHex` from: Delegate protocol, DamusState, SaveKeysView, UserSettingsStore, and all test mocks. Includes 5 regression tests that fail before the fix and pass after: - testRemoveRelayDoesNotFallBackToBootstrapList - testCacheBridgesAsyncWriteGap - testRapidSequentialRemovesDoNotReintroduceRelays - testLoadClearsCacheAndReadsFromNdb - testNdbQueryFindsRelayListWithoutStoredHex Changelog-Fixed: Fix stale relay list causing inability to disconnect relays Closes: #3537 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: alltheseas <alltheseas@users.noreply.github.com>
238 lines
10 KiB
Swift
238 lines
10 KiB
Swift
//
|
|
// SaveKeysView.swift
|
|
// damus
|
|
//
|
|
// Created by William Casarin on 2022-05-21.
|
|
//
|
|
|
|
import SwiftUI
|
|
import Security
|
|
|
|
struct SaveKeysView: View {
|
|
let account: CreateAccountModel
|
|
let pool: RelayPool = RelayPool(ndb: Ndb()!)
|
|
@State var loading: Bool = false
|
|
@State var error: String? = nil
|
|
|
|
@State private var credential_handler = CredentialHandler()
|
|
|
|
@FocusState var pubkey_focused: Bool
|
|
@FocusState var privkey_focused: Bool
|
|
|
|
let first_contact_event: NdbNote?
|
|
let first_relay_list_event: NdbNote?
|
|
|
|
init(account: CreateAccountModel) {
|
|
self.account = account
|
|
self.first_contact_event = make_first_contact_event(keypair: account.keypair)
|
|
self.first_relay_list_event = NIP65.RelayList(relays: get_default_bootstrap_relays()).toNostrEvent(keypair: account.full_keypair)
|
|
}
|
|
|
|
var body: some View {
|
|
ZStack(alignment: .top) {
|
|
VStack(alignment: .center) {
|
|
|
|
Spacer()
|
|
|
|
Image("logo-nobg")
|
|
.resizable()
|
|
.shadow(color: DamusColors.purple, radius: 2)
|
|
.frame(width: 56, height: 56, alignment: .center)
|
|
.padding(.top, 20.0)
|
|
|
|
if account.rendered_name.isEmpty {
|
|
Text("Welcome!", comment: "Text to welcome user.")
|
|
.font(.title)
|
|
.fontWeight(.heavy)
|
|
.foregroundStyle(DamusLogoGradient.gradient)
|
|
} else {
|
|
Text("Welcome, \(account.rendered_name)!", comment: "Text to welcome user.")
|
|
.font(.title)
|
|
.fontWeight(.heavy)
|
|
.foregroundStyle(DamusLogoGradient.gradient)
|
|
}
|
|
|
|
Text("Save your login info?", comment: "Ask user if they want to save their account information.")
|
|
.font(.title)
|
|
.fontWeight(.heavy)
|
|
.foregroundColor(DamusColors.neutral6)
|
|
.padding(.top, 5)
|
|
|
|
Text("We'll save your account key, so you won't need to enter it manually next time you log in.", comment: "Reminder to user that they should save their account information.")
|
|
.font(.system(size: 14))
|
|
.foregroundColor(DamusColors.neutral6)
|
|
.padding(.top, 2)
|
|
.padding(.bottom, 100)
|
|
.multilineTextAlignment(.center)
|
|
|
|
Spacer()
|
|
|
|
if loading {
|
|
ProgressView()
|
|
.progressViewStyle(.circular)
|
|
} else if let err = error {
|
|
Text("Error: \(err)", comment: "Error message indicating why saving keys failed.")
|
|
.foregroundColor(.red)
|
|
|
|
Button(action: {
|
|
Task { await complete_account_creation(account) }
|
|
}) {
|
|
HStack {
|
|
Text("Retry", comment: "Button to retry completing account creation after an error occurred.")
|
|
.fontWeight(.semibold)
|
|
}
|
|
.frame(minWidth: 300, maxWidth: .infinity, maxHeight: 12, alignment: .center)
|
|
}
|
|
.buttonStyle(GradientButtonStyle())
|
|
.padding(.top, 20)
|
|
} else {
|
|
|
|
Button(action: {
|
|
save_key(account)
|
|
Task { await complete_account_creation(account) }
|
|
}) {
|
|
HStack {
|
|
Text("Save", comment: "Button to save key, complete account creation, and start using the app.")
|
|
.fontWeight(.semibold)
|
|
}
|
|
.frame(minWidth: 300, maxWidth: .infinity, maxHeight: 12, alignment: .center)
|
|
}
|
|
.buttonStyle(GradientButtonStyle())
|
|
.padding(.top, 20)
|
|
.accessibilityIdentifier(AppAccessibilityIdentifiers.sign_up_save_keys_button.rawValue)
|
|
|
|
Button(action: {
|
|
Task { await complete_account_creation(account) }
|
|
}) {
|
|
HStack {
|
|
Text("Not now", comment: "Button to not save key, complete account creation, and start using the app.")
|
|
.fontWeight(.semibold)
|
|
}
|
|
.frame(minWidth: 300, maxWidth: .infinity, maxHeight: 12, alignment: .center)
|
|
}
|
|
.buttonStyle(NeutralButtonStyle(padding: EdgeInsets(top: 15, leading: 15, bottom: 15, trailing: 15), cornerRadius: 12))
|
|
.padding(.top, 20)
|
|
.accessibilityIdentifier(AppAccessibilityIdentifiers.sign_up_skip_save_keys_button.rawValue)
|
|
}
|
|
}
|
|
.padding(20)
|
|
}
|
|
.background(DamusBackground(maxHeight: UIScreen.main.bounds.size.height/2), alignment: .top)
|
|
.navigationBarBackButtonHidden(true)
|
|
.navigationBarItems(leading: BackNav())
|
|
|
|
}
|
|
|
|
func save_key(_ account: CreateAccountModel) {
|
|
credential_handler.save_credential(pubkey: account.pubkey, privkey: account.privkey)
|
|
}
|
|
|
|
func complete_account_creation(_ account: CreateAccountModel) async {
|
|
guard let first_contact_event else {
|
|
error = NSLocalizedString("Could not create your initial contact list event. This is a software bug, please contact Damus support via support@damus.io or through our Nostr account for help.", comment: "Error message to the user indicating that the initial contact list failed to be created.")
|
|
return
|
|
}
|
|
guard let first_relay_list_event else {
|
|
error = NSLocalizedString("Could not create your initial relay list. This is a software bug, please contact Damus support via support@damus.io or through our Nostr account for help.", comment: "Error message to the user indicating that the initial relay list failed to be created.")
|
|
return
|
|
}
|
|
// Save contact list to storage right away so that we don't need to depend on the network to complete this important step
|
|
self.save_to_storage(first_contact_event: first_contact_event, first_relay_list_event: first_relay_list_event, for: account)
|
|
|
|
let bootstrap_relays = load_bootstrap_relays(pubkey: account.pubkey)
|
|
for relay in bootstrap_relays {
|
|
await add_rw_relay(self.pool, relay)
|
|
}
|
|
|
|
self.loading = true
|
|
|
|
Task {
|
|
await self.pool.connect()
|
|
|
|
let stream = AsyncStream<(RelayURL, NostrConnectionEvent)> { streamContinuation in
|
|
Task { await self.pool.register_handler(sub_id: "signup", filters: nil, handler: streamContinuation) }
|
|
}
|
|
for await (relayUrl, connectionEvent) in stream {
|
|
await handle_event(relay: relayUrl, ev: connectionEvent)
|
|
}
|
|
}
|
|
}
|
|
|
|
func save_to_storage(first_contact_event: NdbNote, first_relay_list_event: NdbNote, for account: CreateAccountModel) {
|
|
// Send to NostrDB so that we have a local copy in storage
|
|
self.pool.send_raw_to_local_ndb(.typical(.event(first_contact_event)))
|
|
self.pool.send_raw_to_local_ndb(.typical(.event(first_relay_list_event)))
|
|
|
|
// Save the ID to user settings so that we can easily find it later.
|
|
let settings = UserSettingsStore.globally_load_for(pubkey: account.pubkey)
|
|
settings.latest_contact_event_id_hex = first_contact_event.id.hex()
|
|
}
|
|
|
|
func handle_event(relay: RelayURL, ev: NostrConnectionEvent) async {
|
|
switch ev {
|
|
case .ws_connection_event(let wsev):
|
|
switch wsev {
|
|
case .connected:
|
|
let metadata = create_account_to_metadata(account)
|
|
|
|
if let keypair = account.keypair.to_full(),
|
|
let metadata_ev = make_metadata_event(keypair: keypair, metadata: metadata) {
|
|
await self.pool.send(.event(metadata_ev))
|
|
}
|
|
|
|
if let first_contact_event {
|
|
await self.pool.send(.event(first_contact_event))
|
|
}
|
|
|
|
if let first_relay_list_event {
|
|
await self.pool.send(.event(first_relay_list_event))
|
|
}
|
|
|
|
do {
|
|
try save_keypair(pubkey: account.pubkey, privkey: account.privkey)
|
|
notify(.login(account.keypair))
|
|
} catch {
|
|
self.error = "Failed to save keys"
|
|
}
|
|
|
|
case .error(let err):
|
|
self.loading = false
|
|
self.error = String(describing: err)
|
|
default:
|
|
break
|
|
}
|
|
case .nostr_event(let resp):
|
|
switch resp {
|
|
case .notice(let msg):
|
|
// TODO handle message
|
|
self.loading = false
|
|
self.error = msg
|
|
print(msg)
|
|
case .event:
|
|
print("event in signup?")
|
|
case .eose:
|
|
break
|
|
case .ok:
|
|
break
|
|
case .auth:
|
|
break
|
|
case .negentropyError:
|
|
break // Not relevant during signup
|
|
case .negentropyMessage:
|
|
break // Not relevant during signup
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct SaveKeysView_Previews: PreviewProvider {
|
|
static var previews: some View {
|
|
let model = CreateAccountModel(display_name: "William", name: "jb55", about: "I'm me")
|
|
SaveKeysView(account: model)
|
|
}
|
|
}
|
|
|
|
func create_account_to_metadata(_ model: CreateAccountModel) -> Profile {
|
|
return Profile(name: model.name, display_name: model.display_name, about: model.about, picture: model.profile_image?.absoluteString, banner: nil, website: nil, lud06: nil, lud16: nil, nip05: nil, damus_donation: nil)
|
|
}
|