Fix onboarding crash

This commit fixes a crash that occurred when clicking "follow all"
during onboarding.

This fix works by making `Contacts` and `PostBox` isolated into a
specific Swift Actor, and updating direct and indirect usages
accordingly.

Changelog-Fixed: Fixed a crash that occurred when clicking "follow all" during onboarding.
Closes: https://github.com/damus-io/damus/issues/3422
Co-authored-by: alltheseas <64376233+alltheseas@users.noreply.github.com>
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
This commit is contained in:
Daniel D’Aquino
2026-01-05 15:57:20 -08:00
parent 368f94a209
commit 71c36052e2
31 changed files with 96 additions and 39 deletions

View File

@@ -7,6 +7,7 @@
import Foundation import Foundation
@MainActor
struct NotificationExtensionState: HeadlessDamusState { struct NotificationExtensionState: HeadlessDamusState {
let ndb: Ndb let ndb: Ndb
let settings: UserSettingsStore let settings: UserSettingsStore

View File

@@ -44,7 +44,8 @@ class NotificationService: UNNotificationServiceExtension {
// Log that we got a push notification // Log that we got a push notification
Log.debug("Got nostr event push notification from pubkey %s", for: .push_notifications, nostr_event.pubkey.hex()) Log.debug("Got nostr event push notification from pubkey %s", for: .push_notifications, nostr_event.pubkey.hex())
guard let state = NotificationExtensionState() else { Task {
guard let state = await NotificationExtensionState() else {
Log.debug("Failed to open nostrdb", for: .push_notifications) Log.debug("Failed to open nostrdb", for: .push_notifications)
// Something failed to initialize so let's go for the next best thing // Something failed to initialize so let's go for the next best thing
@@ -67,8 +68,6 @@ class NotificationService: UNNotificationServiceExtension {
}() }()
let sender_pubkey = nostr_event.pubkey let sender_pubkey = nostr_event.pubkey
Task {
// Don't show notification details that match mute list. // Don't show notification details that match mute list.
// TODO: Remove this code block once we get notification suppression entitlement from Apple. It will be covered by the `guard should_display_notification` block // TODO: Remove this code block once we get notification suppression entitlement from Apple. It will be covered by the `guard should_display_notification` block
if await state.mutelist_manager.is_event_muted(nostr_event) { if await state.mutelist_manager.is_event_muted(nostr_event) {

View File

@@ -1015,6 +1015,7 @@ func timeline_name(_ timeline: Timeline?) -> String {
} }
@discardableResult @discardableResult
@MainActor
func handle_unfollow(state: DamusState, unfollow: FollowRef) async -> Bool { func handle_unfollow(state: DamusState, unfollow: FollowRef) async -> Bool {
guard let keypair = state.keypair.to_full() else { guard let keypair = state.keypair.to_full() else {
return false return false
@@ -1043,6 +1044,7 @@ func handle_unfollow(state: DamusState, unfollow: FollowRef) async -> Bool {
} }
@discardableResult @discardableResult
@MainActor
func handle_follow(state: DamusState, follow: FollowRef) async -> Bool { func handle_follow(state: DamusState, follow: FollowRef) async -> Bool {
guard let keypair = state.keypair.to_full() else { guard let keypair = state.keypair.to_full() else {
return false return false
@@ -1071,9 +1073,9 @@ func handle_follow(state: DamusState, follow: FollowRef) async -> Bool {
func handle_follow_notif(state: DamusState, target: FollowTarget) async -> Bool { func handle_follow_notif(state: DamusState, target: FollowTarget) async -> Bool {
switch target { switch target {
case .pubkey(let pk): case .pubkey(let pk):
state.contacts.add_friend_pubkey(pk) await state.contacts.add_friend_pubkey(pk)
case .contact(let ev): case .contact(let ev):
state.contacts.add_friend_contact(ev) await state.contacts.add_friend_contact(ev)
} }
return await handle_follow(state: state, follow: target.follow_ref) return await handle_follow(state: state, follow: target.follow_ref)

View File

@@ -191,6 +191,7 @@ extension NostrNetworkManager {
/// The latest contact list `NostrEvent` /// The latest contact list `NostrEvent`
/// ///
/// Note: Read-only access, because `NostrNetworkManager` does not manage contact lists. /// Note: Read-only access, because `NostrNetworkManager` does not manage contact lists.
@MainActor
var latestContactListEvent: NostrEvent? { get } var latestContactListEvent: NostrEvent? { get }
/// Default bootstrap relays to start with when a user relay list is not present /// Default bootstrap relays to start with when a user relay list is not present

View File

@@ -30,6 +30,7 @@ extension NostrNetworkManager {
// MARK: - Computing the relays to connect to // MARK: - Computing the relays to connect to
@MainActor
private func relaysToConnectTo() -> [RelayPool.RelayDescriptor] { private func relaysToConnectTo() -> [RelayPool.RelayDescriptor] {
return self.computeRelaysToConnectTo(with: self.getBestEffortRelayList()) return self.computeRelaysToConnectTo(with: self.getBestEffortRelayList())
} }
@@ -49,6 +50,7 @@ extension NostrNetworkManager {
/// It attempts to get a relay list from the user. If one is not available, it uses the default bootstrap list. /// It attempts to get a relay list from the user. If one is not available, it uses the default bootstrap list.
/// ///
/// This is always guaranteed to return a relay list. /// This is always guaranteed to return a relay list.
@MainActor
func getBestEffortRelayList() -> NIP65.RelayList { func getBestEffortRelayList() -> NIP65.RelayList {
guard let userCurrentRelayList = self.getUserCurrentRelayList() else { guard let userCurrentRelayList = self.getUserCurrentRelayList() else {
return NIP65.RelayList(relays: delegate.bootstrapRelays) return NIP65.RelayList(relays: delegate.bootstrapRelays)
@@ -59,6 +61,7 @@ extension NostrNetworkManager {
/// Gets the user's current relay list. /// Gets the user's current relay list.
/// ///
/// It attempts to get a NIP-65 relay list from the local database, or falls back to a legacy list. /// It attempts to get a NIP-65 relay list from the local database, or falls back to a legacy list.
@MainActor
func getUserCurrentRelayList() -> NIP65.RelayList? { func getUserCurrentRelayList() -> NIP65.RelayList? {
if let latestRelayListEvent = try? self.getLatestNIP65RelayList() { return latestRelayListEvent } if let latestRelayListEvent = try? self.getLatestNIP65RelayList() { return latestRelayListEvent }
if let latestRelayListEvent = try? self.getLatestKind3RelayList() { return latestRelayListEvent } if let latestRelayListEvent = try? self.getLatestKind3RelayList() { return latestRelayListEvent }
@@ -93,6 +96,7 @@ extension NostrNetworkManager {
/// Gets the latest `kind:3` relay list from NostrDB. /// Gets the latest `kind:3` relay list from NostrDB.
/// ///
/// This is `private` because it is part of internal logic. Callers should use the higher level functions. /// This is `private` because it is part of internal logic. Callers should use the higher level functions.
@MainActor
private func getLatestKind3RelayList() throws(LoadingError) -> NIP65.RelayList? { private func getLatestKind3RelayList() throws(LoadingError) -> NIP65.RelayList? {
guard let latestContactListEvent = delegate.latestContactListEvent else { return nil } guard let latestContactListEvent = delegate.latestContactListEvent else { return nil }
guard let legacyContactList = try? NIP65.RelayList.fromLegacyContactList(latestContactListEvent) else { throw .relayListParseError } guard let legacyContactList = try? NIP65.RelayList.fromLegacyContactList(latestContactListEvent) else { throw .relayListParseError }
@@ -114,6 +118,7 @@ extension NostrNetworkManager {
/// Gets the creation date of the user's current relay list, with preference to NIP-65 relay lists /// Gets the creation date of the user's current relay list, with preference to NIP-65 relay lists
/// - Returns: The current relay list's creation date /// - Returns: The current relay list's creation date
@MainActor
private func getUserCurrentRelayListCreationDate() -> UInt32? { private func getUserCurrentRelayListCreationDate() -> UInt32? {
if let latestNIP65RelayListEvent = self.getLatestNIP65RelayListEvent() { return latestNIP65RelayListEvent.created_at } if let latestNIP65RelayListEvent = self.getLatestNIP65RelayListEvent() { return latestNIP65RelayListEvent.created_at }
if let latestKind3RelayListEvent = delegate.latestContactListEvent { return latestKind3RelayListEvent.created_at } if let latestKind3RelayListEvent = delegate.latestContactListEvent { return latestKind3RelayListEvent.created_at }
@@ -134,7 +139,7 @@ extension NostrNetworkManager {
func listenAndHandleRelayUpdates() async { func listenAndHandleRelayUpdates() async {
let filter = NostrFilter(kinds: [.relay_list], authors: [delegate.keypair.pubkey]) let filter = NostrFilter(kinds: [.relay_list], authors: [delegate.keypair.pubkey])
for await noteLender in self.reader.streamIndefinitely(filters: [filter]) { for await noteLender in self.reader.streamIndefinitely(filters: [filter]) {
let currentRelayListCreationDate = self.getUserCurrentRelayListCreationDate() let currentRelayListCreationDate = await self.getUserCurrentRelayListCreationDate()
guard let note = noteLender.justGetACopy() else { continue } guard let note = noteLender.justGetACopy() else { continue }
guard note.pubkey == self.delegate.keypair.pubkey else { continue } // Ensure this new list was ours guard note.pubkey == self.delegate.keypair.pubkey else { continue } // Ensure this new list was ours
guard note.created_at > (currentRelayListCreationDate ?? 0) else { continue } // Ensure this is a newer list guard note.created_at > (currentRelayListCreationDate ?? 0) else { continue } // Ensure this is a newer list
@@ -147,7 +152,7 @@ extension NostrNetworkManager {
// MARK: - Editing the user's relay list // MARK: - Editing the user's relay list
func upsert(relay: NIP65.RelayList.RelayItem, force: Bool = false, overwriteExisting: Bool = false) async throws(UpdateError) { func upsert(relay: NIP65.RelayList.RelayItem, force: Bool = false, overwriteExisting: Bool = false) async throws(UpdateError) {
guard let currentUserRelayList = force ? self.getBestEffortRelayList() : self.getUserCurrentRelayList() else { throw .noInitialRelayList } guard let currentUserRelayList = await force ? self.getBestEffortRelayList() : self.getUserCurrentRelayList() else { throw .noInitialRelayList }
guard !currentUserRelayList.relays.keys.contains(relay.url) || overwriteExisting else { throw .relayAlreadyExists } guard !currentUserRelayList.relays.keys.contains(relay.url) || overwriteExisting else { throw .relayAlreadyExists }
var newList = currentUserRelayList.relays var newList = currentUserRelayList.relays
newList[relay.url] = relay newList[relay.url] = relay
@@ -155,13 +160,13 @@ extension NostrNetworkManager {
} }
func insert(relay: NIP65.RelayList.RelayItem, force: Bool = false) async throws(UpdateError) { func insert(relay: NIP65.RelayList.RelayItem, force: Bool = false) async throws(UpdateError) {
guard let currentUserRelayList = force ? self.getBestEffortRelayList() : self.getUserCurrentRelayList() else { throw .noInitialRelayList } guard let currentUserRelayList = await force ? self.getBestEffortRelayList() : self.getUserCurrentRelayList() else { throw .noInitialRelayList }
guard currentUserRelayList.relays[relay.url] == nil else { throw .relayAlreadyExists } guard currentUserRelayList.relays[relay.url] == nil else { throw .relayAlreadyExists }
try await self.upsert(relay: relay, force: force) try await self.upsert(relay: relay, force: force)
} }
func remove(relayURL: RelayURL, force: Bool = false) async throws(UpdateError) { func remove(relayURL: RelayURL, force: Bool = false) async throws(UpdateError) {
guard let currentUserRelayList = force ? self.getBestEffortRelayList() : self.getUserCurrentRelayList() else { throw .noInitialRelayList } guard let currentUserRelayList = await force ? self.getBestEffortRelayList() : self.getUserCurrentRelayList() else { throw .noInitialRelayList }
guard currentUserRelayList.relays.keys.contains(relayURL) || force else { throw .noSuchRelay } guard currentUserRelayList.relays.keys.contains(relayURL) || force else { throw .noSuchRelay }
var newList = currentUserRelayList.relays var newList = currentUserRelayList.relays
newList[relayURL] = nil newList[relayURL] = nil

View File

@@ -177,6 +177,7 @@ class DamusState: HeadlessDamusState, ObservableObject {
} }
} }
@MainActor
static var empty: DamusState { static var empty: DamusState {
let empty_pub: Pubkey = .empty let empty_pub: Pubkey = .empty
let empty_sec: Privkey = .empty let empty_sec: Privkey = .empty
@@ -226,6 +227,7 @@ fileprivate extension DamusState {
set { self.settings.latestRelayListEventIdHex = newValue } set { self.settings.latestRelayListEventIdHex = newValue }
} }
@MainActor
var latestContactListEvent: NostrEvent? { self.contacts.event } var latestContactListEvent: NostrEvent? { self.contacts.event }
var bootstrapRelays: [RelayURL] { get_default_bootstrap_relays() } var bootstrapRelays: [RelayURL] { get_default_bootstrap_relays() }
var developerMode: Bool { self.settings.developer_mode } var developerMode: Bool { self.settings.developer_mode }

View File

@@ -122,6 +122,7 @@ struct DirectMessagesView: View {
} }
} }
@MainActor
func would_filter_non_friends_from_dms(contacts: Contacts, dms: [DirectMessageModel]) -> Bool { func would_filter_non_friends_from_dms(contacts: Contacts, dms: [DirectMessageModel]) -> Bool {
for dm in dms { for dm in dms {
if !FriendFilter.friends_of_friends.filter(contacts: contacts, pubkey: dm.pubkey) { if !FriendFilter.friends_of_friends.filter(contacts: contacts, pubkey: dm.pubkey) {

View File

@@ -58,6 +58,7 @@ struct EventView: View {
} }
// blame the porn bots for this code // blame the porn bots for this code
@MainActor
func should_blur_images(settings: UserSettingsStore, contacts: Contacts, ev: NostrEvent, our_pubkey: Pubkey, booster_pubkey: Pubkey? = nil) -> Bool { func should_blur_images(settings: UserSettingsStore, contacts: Contacts, ev: NostrEvent, our_pubkey: Pubkey, booster_pubkey: Pubkey? = nil) -> Bool {
if settings.undistractMode { if settings.undistractMode {
return true return true
@@ -80,6 +81,7 @@ func should_blur_images(settings: UserSettingsStore, contacts: Contacts, ev: Nos
} }
// blame the porn bots for this code too // blame the porn bots for this code too
@MainActor
func should_blur_images(damus_state: DamusState, ev: NostrEvent) -> Bool { func should_blur_images(damus_state: DamusState, ev: NostrEvent) -> Bool {
return should_blur_images( return should_blur_images(
settings: damus_state.settings, settings: damus_state.settings,

View File

@@ -7,6 +7,7 @@
import Foundation import Foundation
@MainActor
class Contacts { class Contacts {
private var friends: Set<Pubkey> = Set() private var friends: Set<Pubkey> = Set()
private var friend_of_friends: Set<Pubkey> = Set() private var friend_of_friends: Set<Pubkey> = Set()

View File

@@ -46,7 +46,7 @@ class NIP05DomainEventsModel: ObservableObject {
filter.kinds = [.text, .longform, .highlight] filter.kinds = [.text, .longform, .highlight]
var authors = Set<Pubkey>() var authors = Set<Pubkey>()
for pubkey in state.contacts.get_friend_of_friends_list() { for pubkey in await state.contacts.get_friend_of_friends_list() {
guard let profile = try? state.profiles.lookup(id: pubkey), guard let profile = try? state.profiles.lookup(id: pubkey),
let nip05_str = profile.nip05, let nip05_str = profile.nip05,
let nip05 = NIP05.parse(nip05_str), let nip05 = NIP05.parse(nip05_str),

View File

@@ -36,7 +36,7 @@ func should_display_notification(state: HeadlessDamusState, event ev: NostrEvent
} }
if state.settings.notification_only_from_following, if state.settings.notification_only_from_following,
state.contacts.follow_state(ev.pubkey) != .follows await state.contacts.follow_state(ev.pubkey) != .follows
{ {
return false return false
} }

View File

@@ -33,6 +33,7 @@ class NotificationFilter: ObservableObject, Equatable {
self.hellthread_notification_max_pubkeys = hellthread_notification_max_pubkeys self.hellthread_notification_max_pubkeys = hellthread_notification_max_pubkeys
} }
@MainActor
func filter(contacts: Contacts, items: [NotificationItem]) -> [NotificationItem] { func filter(contacts: Contacts, items: [NotificationItem]) -> [NotificationItem] {
return items.reduce(into: []) { acc, item in return items.reduce(into: []) { acc, item in
@@ -214,6 +215,7 @@ struct NotificationsView_Previews: PreviewProvider {
} }
} }
@MainActor
func would_filter_non_friends_from_notifications(contacts: Contacts, state: NotificationFilterState, items: [NotificationItem]) -> Bool { func would_filter_non_friends_from_notifications(contacts: Contacts, state: NotificationFilterState, items: [NotificationItem]) -> Bool {
for item in items { for item in items {
// this is only valid depending on which tab we're looking at // this is only valid depending on which tab we're looking at

View File

@@ -53,7 +53,7 @@ enum CancelSendErr {
case too_late case too_late
} }
class PostBox { actor PostBox {
private let pool: RelayPool private let pool: RelayPool
var events: [NoteId: PostedEvent] var events: [NoteId: PostedEvent]
@@ -65,7 +65,7 @@ class PostBox {
Task { await self.pool.register_handler(sub_id: "postbox", filters: nil, to: nil, handler: streamContinuation) } Task { await self.pool.register_handler(sub_id: "postbox", filters: nil, to: nil, handler: streamContinuation) }
} }
for await (relayUrl, connectionEvent) in stream { for await (relayUrl, connectionEvent) in stream {
handle_event(relay_id: relayUrl, connectionEvent) await handle_event(relay_id: relayUrl, connectionEvent)
} }
} }
} }

View File

@@ -136,6 +136,7 @@ class ProfileModel: ObservableObject, Equatable {
conversationListener = nil conversationListener = nil
} }
@MainActor
func handle_profile_contact_event(_ ev: NostrEvent) { func handle_profile_contact_event(_ ev: NostrEvent) {
process_contact_event(state: damus, ev: ev) process_contact_event(state: damus, ev: ev)

View File

@@ -20,6 +20,7 @@ enum FriendType {
} }
} }
@MainActor
func get_friend_type(contacts: Contacts, pubkey: Pubkey) -> FriendType? { func get_friend_type(contacts: Contacts, pubkey: Pubkey) -> FriendType? {
if contacts.is_friend_or_self(pubkey) { if contacts.is_friend_or_self(pubkey) {
return .friend return .friend

View File

@@ -141,6 +141,7 @@ func get_profile_url(picture: String?, pubkey: Pubkey, profiles: Profiles) -> UR
return URL(string: robohash(pubkey))! return URL(string: robohash(pubkey))!
} }
@MainActor
func make_preview_profiles(_ pubkey: Pubkey) -> Profiles { func make_preview_profiles(_ pubkey: Pubkey) -> Profiles {
let profiles = Profiles(ndb: test_damus_state.ndb) let profiles = Profiles(ndb: test_damus_state.ndb)
//let picture = "http://cdn.jb55.com/img/red-me.jpg" //let picture = "http://cdn.jb55.com/img/red-me.jpg"

View File

@@ -206,7 +206,7 @@ struct SearchResultsView_Previews: PreviewProvider {
} }
*/ */
@MainActor
func search_for_string(profiles: Profiles, contacts: Contacts, search new: String) -> Search? { func search_for_string(profiles: Profiles, contacts: Contacts, search new: String) -> Search? {
guard new.count != 0 else { guard new.count != 0 else {
return nil return nil
@@ -267,6 +267,7 @@ func make_hashtagable(_ str: String) -> String {
return String(new.filter{$0 != " "}) return String(new.filter{$0 != " "})
} }
@MainActor
func search_profiles(profiles: Profiles, contacts: Contacts, search: String) -> [Pubkey] { func search_profiles(profiles: Profiles, contacts: Contacts, search: String) -> [Pubkey] {
// Search by hex pubkey. // Search by hex pubkey.
if let pubkey = hex_decode_pubkey(search), if let pubkey = hex_decode_pubkey(search),

View File

@@ -23,6 +23,7 @@ enum FriendFilter: String, StringCodable {
self.rawValue self.rawValue
} }
@MainActor
func filter(contacts: Contacts, pubkey: Pubkey) -> Bool { func filter(contacts: Contacts, pubkey: Pubkey) -> Bool {
switch self { switch self {
case .all: case .all:

View File

@@ -26,6 +26,7 @@ enum HomeResubFilter {
return nil return nil
} }
@MainActor
func filter(contacts: Contacts, ev: NostrEvent) -> Bool { func filter(contacts: Contacts, ev: NostrEvent) -> Bool {
switch self { switch self {
case .pubkey(let pk): case .pubkey(let pk):
@@ -340,7 +341,7 @@ class HomeModel: ContactsDelegate, ObservableObject {
// since command results are not returned for ephemeral events, // since command results are not returned for ephemeral events,
// remove the request from the postbox which is likely failing over and over // remove the request from the postbox which is likely failing over and over
if damus_state.nostrNetwork.postbox.remove_relayer(relay_id: nwc.relay, event_id: resp.req_id) { if await damus_state.nostrNetwork.postbox.remove_relayer(relay_id: nwc.relay, event_id: resp.req_id) {
Log.debug("HomeModel: got NWC response, removed %s from the postbox", for: .nwc, resp.req_id.hex()) Log.debug("HomeModel: got NWC response, removed %s from the postbox", for: .nwc, resp.req_id.hex())
} else { } else {
Log.debug("HomeModel: got NWC response, %s not found in the postbox, nothing to remove", for: .nwc, resp.req_id.hex()) Log.debug("HomeModel: got NWC response, %s not found in the postbox, nothing to remove", for: .nwc, resp.req_id.hex())
@@ -925,6 +926,7 @@ func update_signal_from_pool(signal: SignalModel, pool: RelayPool) async {
} }
} }
@MainActor
func add_contact_if_friend(contacts: Contacts, ev: NostrEvent) { func add_contact_if_friend(contacts: Contacts, ev: NostrEvent) {
if !contacts.is_friend(ev.pubkey) { if !contacts.is_friend(ev.pubkey) {
return return
@@ -933,6 +935,7 @@ func add_contact_if_friend(contacts: Contacts, ev: NostrEvent) {
contacts.add_friend_contact(ev) contacts.add_friend_contact(ev)
} }
@MainActor
func load_our_contacts(state: DamusState, m_old_ev: NostrEvent?, ev: NostrEvent) { func load_our_contacts(state: DamusState, m_old_ev: NostrEvent?, ev: NostrEvent) {
let contacts = state.contacts let contacts = state.contacts
let new_refs = Set<FollowRef>(ev.referenced_follows) let new_refs = Set<FollowRef>(ev.referenced_follows)
@@ -1043,6 +1046,7 @@ func robohash(_ pk: Pubkey) -> String {
return "https://robohash.org/" + pk.hex() return "https://robohash.org/" + pk.hex()
} }
@MainActor
func load_our_stuff(state: DamusState, ev: NostrEvent) { func load_our_stuff(state: DamusState, ev: NostrEvent) {
guard ev.pubkey == state.pubkey else { guard ev.pubkey == state.pubkey else {
return return
@@ -1061,6 +1065,7 @@ func load_our_stuff(state: DamusState, ev: NostrEvent) {
load_our_contacts(state: state, m_old_ev: m_old_ev, ev: ev) load_our_contacts(state: state, m_old_ev: m_old_ev, ev: ev)
} }
@MainActor
func process_contact_event(state: DamusState, ev: NostrEvent) { func process_contact_event(state: DamusState, ev: NostrEvent) {
load_our_stuff(state: state, ev: ev) load_our_stuff(state: state, ev: ev)
add_contact_if_friend(contacts: state.contacts, ev: ev) add_contact_if_friend(contacts: state.contacts, ev: ev)

View File

@@ -144,6 +144,7 @@ struct WalletView: View {
} }
} }
@MainActor
let test_wallet_connect_url = WalletConnectURL(pubkey: test_pubkey, relay: .init("wss://relay.damus.io")!, keypair: test_damus_state.keypair.to_full()!, lud16: "jb55@sendsats.com") let test_wallet_connect_url = WalletConnectURL(pubkey: test_pubkey, relay: .init("wss://relay.damus.io")!, keypair: test_damus_state.keypair.to_full()!, lud16: "jb55@sendsats.com")
struct WalletView_Previews: PreviewProvider { struct WalletView_Previews: PreviewProvider {

View File

@@ -70,7 +70,7 @@ struct NoteZapButton: View {
return Color.orange return Color.orange
} }
func tap() { func tap() async {
guard let our_zap else { guard let our_zap else {
Task { await send_zap(damus_state: damus_state, target: target, lnurl: lnurl, is_custom: false, comment: nil, amount_sats: nil, zap_type: damus_state.settings.default_zap_type) } Task { await send_zap(damus_state: damus_state, target: target, lnurl: lnurl, is_custom: false, comment: nil, amount_sats: nil, zap_type: damus_state.settings.default_zap_type) }
return return
@@ -84,7 +84,7 @@ struct NoteZapButton: View {
print("cancel_zap: we already have a real zap, can't cancel") print("cancel_zap: we already have a real zap, can't cancel")
break break
case .pending(let pzap): case .pending(let pzap):
guard let res = cancel_zap(zap: pzap, box: damus_state.nostrNetwork.postbox, zapcache: damus_state.zaps, evcache: damus_state.events) else { guard let res = await cancel_zap(zap: pzap, box: damus_state.nostrNetwork.postbox, zapcache: damus_state.zaps, evcache: damus_state.events) else {
UIImpactFeedbackGenerator(style: .soft).impactOccurred() UIImpactFeedbackGenerator(style: .soft).impactOccurred()
return return
@@ -146,7 +146,7 @@ struct NoteZapButton: View {
.highPriorityGesture(TapGesture().onEnded { .highPriorityGesture(TapGesture().onEnded {
guard !damus_state.settings.nozaps else { return } guard !damus_state.settings.nozaps else { return }
tap() Task { await tap() }
}) })
} }
} }
@@ -276,7 +276,7 @@ enum CancelZapErr {
case not_nwc case not_nwc
} }
func cancel_zap(zap: PendingZap, box: PostBox, zapcache: Zaps, evcache: EventCache) -> CancelZapErr? { func cancel_zap(zap: PendingZap, box: PostBox, zapcache: Zaps, evcache: EventCache) async -> CancelZapErr? {
guard case .nwc(let nwc_state) = zap.state else { guard case .nwc(let nwc_state) = zap.state else {
return .not_nwc return .not_nwc
} }
@@ -298,7 +298,7 @@ func cancel_zap(zap: PendingZap, box: PostBox, zapcache: Zaps, evcache: EventCac
return .already_confirmed return .already_confirmed
case .postbox_pending(let nwc_req): case .postbox_pending(let nwc_req):
if let err = box.cancel_send(evid: nwc_req.id) { if let err = await box.cancel_send(evid: nwc_req.id) {
return .send_err(err) return .send_err(err)
} }
let reqid = ZapRequestId(from_pending: zap) let reqid = ZapRequestId(from_pending: zap)

View File

@@ -109,6 +109,7 @@ extension View {
} }
} }
@MainActor
func use_nip05_color(pubkey: Pubkey, contacts: Contacts) -> Bool { func use_nip05_color(pubkey: Pubkey, contacts: Contacts) -> Bool {
return contacts.is_friend_or_self(pubkey) ? true : false return contacts.is_friend_or_self(pubkey) ? true : false
} }

View File

@@ -63,9 +63,11 @@ let test_private_zap = Zap(event: test_note, invoice: test_zap_invoice, zapper:
let test_pending_zap = PendingZap(amount_msat: 10000, target: .note(id: test_note.id, author: test_note.pubkey), request: .normal(test_zap_request), type: .pub, state: .external(.init(state: .fetching_invoice))) let test_pending_zap = PendingZap(amount_msat: 10000, target: .note(id: test_note.id, author: test_note.pubkey), request: .normal(test_zap_request), type: .pub, state: .external(.init(state: .fetching_invoice)))
@MainActor
let test_following_model = FollowingModel(damus_state: test_damus_state, contacts: [test_pubkey, test_pubkey_2], hashtags: [Hashtag(hashtag: "grownostr"), Hashtag(hashtag: "zapathon")]) let test_following_model = FollowingModel(damus_state: test_damus_state, contacts: [test_pubkey, test_pubkey_2], hashtags: [Hashtag(hashtag: "grownostr"), Hashtag(hashtag: "zapathon")])
@MainActor
var test_damus_state: DamusState = ({ var test_damus_state: DamusState = ({
// Create a unique temporary directory // Create a unique temporary directory
var tempDir: String! var tempDir: String!

View File

@@ -23,6 +23,7 @@ final class DamusCacheManagerTests: XCTestCase {
} }
/// Simple smoke test to check if clearing cache will crash the system /// Simple smoke test to check if clearing cache will crash the system
@MainActor
func testCacheManagerSmoke() throws { func testCacheManagerSmoke() throws {
for _ in Range(0...20) { for _ in Range(0...20) {
DamusCacheManager.shared.clear_cache(damus_state: test_damus_state) DamusCacheManager.shared.clear_cache(damus_state: test_damus_state)

View File

@@ -18,6 +18,7 @@ final class EventGroupViewTests: XCTestCase {
// Put teardown code here. This method is called after the invocation of each test method in the class. // Put teardown code here. This method is called after the invocation of each test method in the class.
} }
@MainActor
func testEventAuthorName() { func testEventAuthorName() {
let damusState = test_damus_state let damusState = test_damus_state
let damus_name = "17ldvg64:nq5mhr77" let damus_name = "17ldvg64:nq5mhr77"
@@ -26,6 +27,7 @@ final class EventGroupViewTests: XCTestCase {
XCTAssertEqual(event_author_name(profiles: damusState.profiles, pubkey: ANON_PUBKEY), "Anonymous") XCTAssertEqual(event_author_name(profiles: damusState.profiles, pubkey: ANON_PUBKEY), "Anonymous")
} }
@MainActor
func testEventGroupUniquePubkeys() { func testEventGroupUniquePubkeys() {
let damusState = test_damus_state let damusState = test_damus_state
@@ -48,6 +50,7 @@ final class EventGroupViewTests: XCTestCase {
XCTAssertEqual(event_group_unique_pubkeys(profiles: damusState.profiles, group: .repost(EventGroup(events: [repost1, repost2, repost3]))), [pk1, pk2, pk3]) XCTAssertEqual(event_group_unique_pubkeys(profiles: damusState.profiles, group: .repost(EventGroup(events: [repost1, repost2, repost3]))), [pk1, pk2, pk3])
} }
@MainActor
func testReactingToText() throws { func testReactingToText() throws {
let enUsLocale = Locale(identifier: "en-US") let enUsLocale = Locale(identifier: "en-US")
let damusState = test_damus_state let damusState = test_damus_state

View File

@@ -23,6 +23,7 @@ final class ContactCardManagerTests: XCTestCase {
XCTAssertFalse(result) XCTAssertFalse(result)
} }
@MainActor
func testIsFavorite_WhenPubkeyExists_ReturnsTrue() { func testIsFavorite_WhenPubkeyExists_ReturnsTrue() {
// Given: A pubkey added to favorites // Given: A pubkey added to favorites
let sut = ContactCardManager() let sut = ContactCardManager()
@@ -36,6 +37,7 @@ final class ContactCardManagerTests: XCTestCase {
XCTAssertTrue(result) XCTAssertTrue(result)
} }
@MainActor
func testIsFavorite_WhenPubkeyDoesNotExist_ReturnsFalse() { func testIsFavorite_WhenPubkeyDoesNotExist_ReturnsFalse() {
// Given: A different pubkey added to favorites // Given: A different pubkey added to favorites
let sut = ContactCardManager() let sut = ContactCardManager()
@@ -50,6 +52,7 @@ final class ContactCardManagerTests: XCTestCase {
XCTAssertFalse(result) XCTAssertFalse(result)
} }
@MainActor
func testToggleFavorite_WhenNotFavorite_AddsToFavorites() { func testToggleFavorite_WhenNotFavorite_AddsToFavorites() {
// Given: A pubkey not in favorites // Given: A pubkey not in favorites
let sut = ContactCardManager() let sut = ContactCardManager()
@@ -64,6 +67,7 @@ final class ContactCardManagerTests: XCTestCase {
XCTAssertEqual(sut.favorites.count, 1) XCTAssertEqual(sut.favorites.count, 1)
} }
@MainActor
func testToggleFavorite_WhenAlreadyFavorite_RemovesFromFavorites() { func testToggleFavorite_WhenAlreadyFavorite_RemovesFromFavorites() {
// Given: A pubkey already in favorites // Given: A pubkey already in favorites
let sut = ContactCardManager() let sut = ContactCardManager()
@@ -105,6 +109,7 @@ final class ContactCardManagerTests: XCTestCase {
XCTAssertTrue(sut.isFavorite(targetPubkey)) XCTAssertTrue(sut.isFavorite(targetPubkey))
} }
@MainActor
func testloadEvent_WithContactCard_RemovesFromFavorites() { func testloadEvent_WithContactCard_RemovesFromFavorites() {
// Given: A contact card event without favorite tag (unfavorite) // Given: A contact card event without favorite tag (unfavorite)
let sut = ContactCardManager() let sut = ContactCardManager()
@@ -219,6 +224,7 @@ final class ContactCardManagerTests: XCTestCase {
XCTAssertTrue(sut.isFavorite(targetPubkey)) XCTAssertTrue(sut.isFavorite(targetPubkey))
} }
@MainActor
func testFilter_WithFavoritePubkey_ReturnsTrue() { func testFilter_WithFavoritePubkey_ReturnsTrue() {
// Given: A pubkey in favorites // Given: A pubkey in favorites
let sut = ContactCardManager() let sut = ContactCardManager()

View File

@@ -10,6 +10,7 @@ import XCTest
@testable import damus @testable import damus
final class MutingTests: XCTestCase { final class MutingTests: XCTestCase {
@MainActor
func testWordMuting() async { func testWordMuting() async {
// Setup some test data // Setup some test data
let test_note = NostrEvent( let test_note = NostrEvent(

View File

@@ -344,6 +344,7 @@ class NoteContentViewTests: XCTestCase {
/// Quick test that exercises the direct parsing methods (i.e. not fetching blocks from nostrdb) from `NdbBlockGroup`, and its bridging code with C. /// Quick test that exercises the direct parsing methods (i.e. not fetching blocks from nostrdb) from `NdbBlockGroup`, and its bridging code with C.
/// The parsing logic itself already has test coverage at the nostrdb level. /// The parsing logic itself already has test coverage at the nostrdb level.
@MainActor
func testDirectBlockParsing() { func testDirectBlockParsing() {
let kp = test_keypair_full let kp = test_keypair_full
let dm: NdbNote = NIP04.create_dm("Test", to_pk: kp.pubkey, tags: [], keypair: kp.to_keypair())! let dm: NdbNote = NIP04.create_dm("Test", to_pk: kp.pubkey, tags: [], keypair: kp.to_keypair())!
@@ -360,24 +361,28 @@ class NoteContentViewTests: XCTestCase {
}) })
} }
@MainActor
func testMentionStr_Pubkey_ContainsAbbreviated() throws { func testMentionStr_Pubkey_ContainsAbbreviated() throws {
let compatibleText = createCompatibleText(test_pubkey.npub) let compatibleText = createCompatibleText(test_pubkey.npub)
assertCompatibleTextHasExpectedString(compatibleText: compatibleText, expected: "17ldvg64:nq5mhr77") assertCompatibleTextHasExpectedString(compatibleText: compatibleText, expected: "17ldvg64:nq5mhr77")
} }
@MainActor
func testMentionStr_Pubkey_ContainsFullBech32() { func testMentionStr_Pubkey_ContainsFullBech32() {
let compatableText = createCompatibleText(test_pubkey.npub) let compatableText = createCompatibleText(test_pubkey.npub)
assertCompatibleTextHasExpectedString(compatibleText: compatableText, expected: test_pubkey.npub) assertCompatibleTextHasExpectedString(compatibleText: compatableText, expected: test_pubkey.npub)
} }
@MainActor
func testMentionStr_Nprofile_ContainsAbbreviated() throws { func testMentionStr_Nprofile_ContainsAbbreviated() throws {
let compatibleText = createCompatibleText("nprofile1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gpp4mhxue69uhhytnc9e3k7mgpz4mhxue69uhkg6nzv9ejuumpv34kytnrdaksjlyr9p") let compatibleText = createCompatibleText("nprofile1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gpp4mhxue69uhhytnc9e3k7mgpz4mhxue69uhkg6nzv9ejuumpv34kytnrdaksjlyr9p")
assertCompatibleTextHasExpectedString(compatibleText: compatibleText, expected: "180cvv07:wsyjh6w6") assertCompatibleTextHasExpectedString(compatibleText: compatibleText, expected: "180cvv07:wsyjh6w6")
} }
@MainActor
func testMentionStr_Nprofile_ContainsFullBech32() throws { func testMentionStr_Nprofile_ContainsFullBech32() throws {
let bech = "nprofile1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gpp4mhxue69uhhytnc9e3k7mgpz4mhxue69uhkg6nzv9ejuumpv34kytnrdaksjlyr9p" let bech = "nprofile1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gpp4mhxue69uhhytnc9e3k7mgpz4mhxue69uhkg6nzv9ejuumpv34kytnrdaksjlyr9p"
let compatibleText = createCompatibleText(bech) let compatibleText = createCompatibleText(bech)
@@ -385,18 +390,21 @@ class NoteContentViewTests: XCTestCase {
assertCompatibleTextHasExpectedString(compatibleText: compatibleText, expected: bech) assertCompatibleTextHasExpectedString(compatibleText: compatibleText, expected: bech)
} }
@MainActor
func testMentionStr_Note_ContainsAbbreviated() { func testMentionStr_Note_ContainsAbbreviated() {
let compatibleText = createCompatibleText(test_note.id.bech32) let compatibleText = createCompatibleText(test_note.id.bech32)
assertCompatibleTextHasExpectedString(compatibleText: compatibleText, expected: "note1qqq:qqn2l0z3") assertCompatibleTextHasExpectedString(compatibleText: compatibleText, expected: "note1qqq:qqn2l0z3")
} }
@MainActor
func testMentionStr_Note_ContainsFullBech32() { func testMentionStr_Note_ContainsFullBech32() {
let compatibleText = createCompatibleText(test_note.id.bech32) let compatibleText = createCompatibleText(test_note.id.bech32)
assertCompatibleTextHasExpectedString(compatibleText: compatibleText, expected: test_note.id.bech32) assertCompatibleTextHasExpectedString(compatibleText: compatibleText, expected: test_note.id.bech32)
} }
@MainActor
func testMentionStr_Nevent_ContainsAbbreviated() { func testMentionStr_Nevent_ContainsAbbreviated() {
let bech = "nevent1qqstna2yrezu5wghjvswqqculvvwxsrcvu7uc0f78gan4xqhvz49d9spr3mhxue69uhkummnw3ez6un9d3shjtn4de6x2argwghx6egpr4mhxue69uhkummnw3ez6ur4vgh8wetvd3hhyer9wghxuet5nxnepm" let bech = "nevent1qqstna2yrezu5wghjvswqqculvvwxsrcvu7uc0f78gan4xqhvz49d9spr3mhxue69uhkummnw3ez6un9d3shjtn4de6x2argwghx6egpr4mhxue69uhkummnw3ez6ur4vgh8wetvd3hhyer9wghxuet5nxnepm"
let compatibleText = createCompatibleText(bech) let compatibleText = createCompatibleText(bech)
@@ -404,6 +412,7 @@ class NoteContentViewTests: XCTestCase {
assertCompatibleTextHasExpectedString(compatibleText: compatibleText, expected: "nevent1q:t5nxnepm") assertCompatibleTextHasExpectedString(compatibleText: compatibleText, expected: "nevent1q:t5nxnepm")
} }
@MainActor
func testMentionStr_Nevent_ContainsFullBech32() throws { func testMentionStr_Nevent_ContainsFullBech32() throws {
let bech = "nevent1qqstna2yrezu5wghjvswqqculvvwxsrcvu7uc0f78gan4xqhvz49d9spr3mhxue69uhkummnw3ez6un9d3shjtn4de6x2argwghx6egpr4mhxue69uhkummnw3ez6ur4vgh8wetvd3hhyer9wghxuet5nxnepm" let bech = "nevent1qqstna2yrezu5wghjvswqqculvvwxsrcvu7uc0f78gan4xqhvz49d9spr3mhxue69uhkummnw3ez6un9d3shjtn4de6x2argwghx6egpr4mhxue69uhkummnw3ez6ur4vgh8wetvd3hhyer9wghxuet5nxnepm"
let compatibleText = createCompatibleText(bech) let compatibleText = createCompatibleText(bech)
@@ -411,6 +420,7 @@ class NoteContentViewTests: XCTestCase {
assertCompatibleTextHasExpectedString(compatibleText: compatibleText, expected: bech) assertCompatibleTextHasExpectedString(compatibleText: compatibleText, expected: bech)
} }
@MainActor
func testMentionStr_Naddr_ContainsAbbreviated() { func testMentionStr_Naddr_ContainsAbbreviated() {
let bech = "naddr1qqxnzdesxqmnxvpexqunzvpcqyt8wumn8ghj7un9d3shjtnwdaehgu3wvfskueqzypve7elhmamff3sr5mgxxms4a0rppkmhmn7504h96pfcdkpplvl2jqcyqqq823cnmhuld" let bech = "naddr1qqxnzdesxqmnxvpexqunzvpcqyt8wumn8ghj7un9d3shjtnwdaehgu3wvfskueqzypve7elhmamff3sr5mgxxms4a0rppkmhmn7504h96pfcdkpplvl2jqcyqqq823cnmhuld"
let compatibleText = createCompatibleText(bech) let compatibleText = createCompatibleText(bech)
@@ -418,6 +428,7 @@ class NoteContentViewTests: XCTestCase {
assertCompatibleTextHasExpectedString(compatibleText: compatibleText, expected: "naddr1qq:3cnmhuld") assertCompatibleTextHasExpectedString(compatibleText: compatibleText, expected: "naddr1qq:3cnmhuld")
} }
@MainActor
func testMentionStr_Naddr_ContainsFullBech32() { func testMentionStr_Naddr_ContainsFullBech32() {
let bech = "naddr1qqxnzdesxqmnxvpexqunzvpcqyt8wumn8ghj7un9d3shjtnwdaehgu3wvfskueqzypve7elhmamff3sr5mgxxms4a0rppkmhmn7504h96pfcdkpplvl2jqcyqqq823cnmhuld" let bech = "naddr1qqxnzdesxqmnxvpexqunzvpcqyt8wumn8ghj7un9d3shjtnwdaehgu3wvfskueqzypve7elhmamff3sr5mgxxms4a0rppkmhmn7504h96pfcdkpplvl2jqcyqqq823cnmhuld"
let compatibleText = createCompatibleText(bech) let compatibleText = createCompatibleText(bech)
@@ -436,6 +447,7 @@ private func assertCompatibleTextHasExpectedString(compatibleText: CompatibleTex
XCTAssertTrue(hasExpected) XCTAssertTrue(hasExpected)
} }
@MainActor
private func createCompatibleText(_ bechString: String) -> CompatibleText { private func createCompatibleText(_ bechString: String) -> CompatibleText {
guard let mentionRef = Bech32Object.parse(bechString)?.toMentionRef() else { guard let mentionRef = Bech32Object.parse(bechString)?.toMentionRef() else {
XCTFail("Failed to create MentionRef from Bech32 string") XCTFail("Failed to create MentionRef from Bech32 string")

View File

@@ -10,6 +10,7 @@ import XCTest
final class RepostedTests: XCTestCase { final class RepostedTests: XCTestCase {
@MainActor
func testPeopleRepostedText() throws { func testPeopleRepostedText() throws {
let enUsLocale = Locale(identifier: "en-US") let enUsLocale = Locale(identifier: "en-US")
let damusState = test_damus_state let damusState = test_damus_state

View File

@@ -94,8 +94,9 @@ final class WalletConnectTests: XCTestCase {
XCTAssertEqual(pool.all_descriptors.count, 1) XCTAssertEqual(pool.all_descriptors.count, 1)
XCTAssertEqual(pool.all_descriptors[0].variant, .nwc) XCTAssertEqual(pool.all_descriptors[0].variant, .nwc)
XCTAssertEqual(pool.all_descriptors[0].url.url.absoluteString, "ws://127.0.0.1") XCTAssertEqual(pool.all_descriptors[0].url.url.absoluteString, "ws://127.0.0.1")
XCTAssertEqual(box.events.count, 1) let boxEventCount = await box.events.count
let ev = box.events.first!.value XCTAssertEqual(boxEventCount, 1)
let ev = await box.events.first!.value
XCTAssertEqual(ev.skip_ephemeral, false) XCTAssertEqual(ev.skip_ephemeral, false)
XCTAssertEqual(ev.remaining.count, 1) XCTAssertEqual(ev.remaining.count, 1)
XCTAssertEqual(ev.remaining[0].relay.url.absoluteString, "ws://127.0.0.1") XCTAssertEqual(ev.remaining[0].relay.url.absoluteString, "ws://127.0.0.1")

View File

@@ -17,6 +17,7 @@ final class ZapTests: XCTestCase {
// Put teardown code here. This method is called after the invocation of each test method in the class. // Put teardown code here. This method is called after the invocation of each test method in the class.
} }
@MainActor
func test_alby_zap() throws { func test_alby_zap() throws {
let zapjson = "eyJjb250ZW50Ijoi4pqhTm9uLWN1c3RvZGlhbCB6YXAgZnJvbSBteSBBbGJ5IEh1YiIsImNyZWF0ZWRfYXQiOjE3MjQ2ODUwNDcsImlkIjoiNGM3NWFiMWU3MDk4Y2NiN2FlYjhmZjdkNDIwMjM2ZDM1N2U1OGNjZmI3OWZiZTEwMTcwNGNiMzY0OTg3YjY4YSIsImtpbmQiOjk3MzUsInB1YmtleSI6Ijc5ZjAwZDNmNWExOWVjODA2MTg5ZmNhYjAzYzFiZTRmZjgxZDE4ZWU0ZjY1M2M4OGZhYzQxZmUwMzU3MGY0MzIiLCJzaWciOiI3OWM5ZDJjN2ExZWI1NmNhZjMyOTY1ZTRkMDJlYjJiYjFmYTY3NGViMDM4ZWE2MmFjZTg2YzBiMzA2OTJhMjU0YWU0M2JhNmMzNjcyMDJkZjgxNzQ5NGNhNTg4NzRkNWI1OWMxY2VhMDdjZTk5Mjk0MmIyOWYwZmVlZmJlM2FiZCIsInRhZ3MiOltbInAiLCIxNWI1Y2Y2Y2RmNGZkMWMwMmYyOGJjY2UwZjE5N2NhZmFlNGM4YzdjNjZhM2UyZTIzYWY5ZmU2MTA4NzUzMTVlIl0sWyJlIiwiYmNiMmZjZmUxYzQ2N2M1ZWM4Mjg1ZTM4NWMzNmVjMTM4Nzk3MDljZWQ5ZDg4MDBjYjM0MGViZjIxOGMzMjEwZCJdLFsiUCIsIjA1MjFkYjk1MzEwOTZkZmY3MDBkY2Y0MTBiMDFkYjQ3YWI2NTk4ZGU3ZTVlZjJjNWEyYmQ3ZTExNjAzMTViZjYiXSxbImJvbHQxMSIsImxuYmMxMHUxcG52ZXhoM2RwdXUyZDJ6bm4wZGNra3hhdG53M2hrZzZ0cGRzczg1Y3RzeXBuOHltbWR5cGtoamd6cGQzMzhqZ3pndzQzcW5wNHEyMjhhMnp0eGt3emF5cHZ6cnNoODIzcW5nbXY5N2YydjlwdXd2dHNhZGV0eXBtdXR5c2N3cHA1dmxjbGwwMHpwcGhoMzJ3OHV0NWpwcDVhMmZtcWg4c3o3bnUyaDd2MDdyMHU1bHN3ZzVsc3NwNXh2YXFlZnpsY2t6bXYwdzg5bHIwazB5dnI1eGQybmc1MmE1cmNkYXJmbTRmMGEwd2dwdXE5cXl5c2dxY3FwY3hxeXo1dnFlcHMzOXNleDUyc2ZtdHU5Z25tNWRhcGs1bGdsZDRwcDk2dXI1YTRhbTk0MHEyNXd6ZHNycmo1MjN4eWEwcnV4YTVscjk2M2cwMjk2cjZtZGZ5MjR2NjUzZXZjcHh5cjBtbWhnd21zcXh2cmhmZCJdLFsicHJlaW1hZ2UiLCJhZDA0N2MwMmZlNWYwNTljODA4NzdkNzk0YmU4OGU0N2M2NDRlYmVkZmRmZTY2M2IyODljOTMxNmRiNDk1ZjJkIl0sWyJkZXNjcmlwdGlvbiIsIntcImtpbmRcIjo5NzM0LFwiY3JlYXRlZF9hdFwiOjE3MjQ2ODUwMzgsXCJjb250ZW50XCI6XCLimqFOb24tY3VzdG9kaWFsIHphcCBmcm9tIG15IEFsYnkgSHViXCIsXCJ0YWdzXCI6W1tcInBcIixcIjE1YjVjZjZjZGY0ZmQxYzAyZjI4YmNjZTBmMTk3Y2FmYWU0YzhjN2M2NmEzZTJlMjNhZjlmZTYxMDg3NTMxNWVcIl0sW1wicmVsYXlzXCIsXCJ3c3M6Ly9wdXJwbGVwYWcuZXMvXCIsXCJ3c3M6Ly9yZWxheS5nZXRhbGJ5LmNvbS92MVwiLFwid3NzOi8vbm9zdHIubW9tL1wiLFwid3NzOi8vbm9zdHIub3h0ci5kZXYvXCIsXCJ3c3M6Ly9ub3MubG9sL1wiLFwid3NzOi8vbm9zdHIud2luZS9cIixcIndzczovL3JlbGF5LmRhbXVzLmlvL1wiLFwid3NzOi8vcmVsYXkubm90b3NoaS53aW4vXCIsXCJ3c3M6Ly9lZGVuLm5vc3RyLmxhbmQvXCJdLFtcImFtb3VudFwiLFwiMTAwMDAwMFwiXSxbXCJlXCIsXCJiY2IyZmNmZTFjNDY3YzVlYzgyODVlMzg1YzM2ZWMxMzg3OTcwOWNlZDlkODgwMGNiMzQwZWJmMjE4YzMyMTBkXCJdXSxcInB1YmtleVwiOlwiMDUyMWRiOTUzMTA5NmRmZjcwMGRjZjQxMGIwMWRiNDdhYjY1OThkZTdlNWVmMmM1YTJiZDdlMTE2MDMxNWJmNlwiLFwiaWRcIjpcIjU3ZDg2MTIwMDc1MjFjMGI1MzJiOTFhZjI0OTgwOTVhMjUxZTYzZjQyNTE4N2U2Yzk1NzAwZmQwYTZiYWI3ZDRcIixcInNpZ1wiOlwiNzk4ZDczNTExOGJjZDE0MjI4YTEyYjZkNTI0MjNmZjI1YmI0ZWQ4Y2Q1ZGFjZjJmNTk3MWVmNTczZmRjM2ZjMDVmYzc5MzE4NWU2OTY4MmNjYTI0M2Q2NGYxNDdhNDQ5ODk2OGEwYmMyODhhZTgzZTc1YzAzZTk5ZjkzNmE2MDNcIn0iXV19Cg==" let zapjson = "eyJjb250ZW50Ijoi4pqhTm9uLWN1c3RvZGlhbCB6YXAgZnJvbSBteSBBbGJ5IEh1YiIsImNyZWF0ZWRfYXQiOjE3MjQ2ODUwNDcsImlkIjoiNGM3NWFiMWU3MDk4Y2NiN2FlYjhmZjdkNDIwMjM2ZDM1N2U1OGNjZmI3OWZiZTEwMTcwNGNiMzY0OTg3YjY4YSIsImtpbmQiOjk3MzUsInB1YmtleSI6Ijc5ZjAwZDNmNWExOWVjODA2MTg5ZmNhYjAzYzFiZTRmZjgxZDE4ZWU0ZjY1M2M4OGZhYzQxZmUwMzU3MGY0MzIiLCJzaWciOiI3OWM5ZDJjN2ExZWI1NmNhZjMyOTY1ZTRkMDJlYjJiYjFmYTY3NGViMDM4ZWE2MmFjZTg2YzBiMzA2OTJhMjU0YWU0M2JhNmMzNjcyMDJkZjgxNzQ5NGNhNTg4NzRkNWI1OWMxY2VhMDdjZTk5Mjk0MmIyOWYwZmVlZmJlM2FiZCIsInRhZ3MiOltbInAiLCIxNWI1Y2Y2Y2RmNGZkMWMwMmYyOGJjY2UwZjE5N2NhZmFlNGM4YzdjNjZhM2UyZTIzYWY5ZmU2MTA4NzUzMTVlIl0sWyJlIiwiYmNiMmZjZmUxYzQ2N2M1ZWM4Mjg1ZTM4NWMzNmVjMTM4Nzk3MDljZWQ5ZDg4MDBjYjM0MGViZjIxOGMzMjEwZCJdLFsiUCIsIjA1MjFkYjk1MzEwOTZkZmY3MDBkY2Y0MTBiMDFkYjQ3YWI2NTk4ZGU3ZTVlZjJjNWEyYmQ3ZTExNjAzMTViZjYiXSxbImJvbHQxMSIsImxuYmMxMHUxcG52ZXhoM2RwdXUyZDJ6bm4wZGNra3hhdG53M2hrZzZ0cGRzczg1Y3RzeXBuOHltbWR5cGtoamd6cGQzMzhqZ3pndzQzcW5wNHEyMjhhMnp0eGt3emF5cHZ6cnNoODIzcW5nbXY5N2YydjlwdXd2dHNhZGV0eXBtdXR5c2N3cHA1dmxjbGwwMHpwcGhoMzJ3OHV0NWpwcDVhMmZtcWg4c3o3bnUyaDd2MDdyMHU1bHN3ZzVsc3NwNXh2YXFlZnpsY2t6bXYwdzg5bHIwazB5dnI1eGQybmc1MmE1cmNkYXJmbTRmMGEwd2dwdXE5cXl5c2dxY3FwY3hxeXo1dnFlcHMzOXNleDUyc2ZtdHU5Z25tNWRhcGs1bGdsZDRwcDk2dXI1YTRhbTk0MHEyNXd6ZHNycmo1MjN4eWEwcnV4YTVscjk2M2cwMjk2cjZtZGZ5MjR2NjUzZXZjcHh5cjBtbWhnd21zcXh2cmhmZCJdLFsicHJlaW1hZ2UiLCJhZDA0N2MwMmZlNWYwNTljODA4NzdkNzk0YmU4OGU0N2M2NDRlYmVkZmRmZTY2M2IyODljOTMxNmRiNDk1ZjJkIl0sWyJkZXNjcmlwdGlvbiIsIntcImtpbmRcIjo5NzM0LFwiY3JlYXRlZF9hdFwiOjE3MjQ2ODUwMzgsXCJjb250ZW50XCI6XCLimqFOb24tY3VzdG9kaWFsIHphcCBmcm9tIG15IEFsYnkgSHViXCIsXCJ0YWdzXCI6W1tcInBcIixcIjE1YjVjZjZjZGY0ZmQxYzAyZjI4YmNjZTBmMTk3Y2FmYWU0YzhjN2M2NmEzZTJlMjNhZjlmZTYxMDg3NTMxNWVcIl0sW1wicmVsYXlzXCIsXCJ3c3M6Ly9wdXJwbGVwYWcuZXMvXCIsXCJ3c3M6Ly9yZWxheS5nZXRhbGJ5LmNvbS92MVwiLFwid3NzOi8vbm9zdHIubW9tL1wiLFwid3NzOi8vbm9zdHIub3h0ci5kZXYvXCIsXCJ3c3M6Ly9ub3MubG9sL1wiLFwid3NzOi8vbm9zdHIud2luZS9cIixcIndzczovL3JlbGF5LmRhbXVzLmlvL1wiLFwid3NzOi8vcmVsYXkubm90b3NoaS53aW4vXCIsXCJ3c3M6Ly9lZGVuLm5vc3RyLmxhbmQvXCJdLFtcImFtb3VudFwiLFwiMTAwMDAwMFwiXSxbXCJlXCIsXCJiY2IyZmNmZTFjNDY3YzVlYzgyODVlMzg1YzM2ZWMxMzg3OTcwOWNlZDlkODgwMGNiMzQwZWJmMjE4YzMyMTBkXCJdXSxcInB1YmtleVwiOlwiMDUyMWRiOTUzMTA5NmRmZjcwMGRjZjQxMGIwMWRiNDdhYjY1OThkZTdlNWVmMmM1YTJiZDdlMTE2MDMxNWJmNlwiLFwiaWRcIjpcIjU3ZDg2MTIwMDc1MjFjMGI1MzJiOTFhZjI0OTgwOTVhMjUxZTYzZjQyNTE4N2U2Yzk1NzAwZmQwYTZiYWI3ZDRcIixcInNpZ1wiOlwiNzk4ZDczNTExOGJjZDE0MjI4YTEyYjZkNTI0MjNmZjI1YmI0ZWQ4Y2Q1ZGFjZjJmNTk3MWVmNTczZmRjM2ZjMDVmYzc5MzE4NWU2OTY4MmNjYTI0M2Q2NGYxNDdhNDQ5ODk2OGEwYmMyODhhZTgzZTc1YzAzZTk5ZjkzNmE2MDNcIn0iXV19Cg=="
@@ -75,6 +76,7 @@ final class ZapTests: XCTestCase {
XCTAssertEqual(message, decrypted.content) XCTAssertEqual(message, decrypted.content)
} }
@MainActor
func testZap() throws { func testZap() throws {
let zapjson = "eyJpZCI6IjUzNmJlZTllODNjODE4ZTNiODJjMTAxOTM1MTI4YWUyN2EwZDQyOTAwMzlhYWYyNTNlZmU1ZjA5MjMyYzE5NjIiLCJwdWJrZXkiOiI5NjMwZjQ2NGNjYTZhNTE0N2FhOGEzNWYwYmNkZDNjZTQ4NTMyNGU3MzJmZDM5ZTA5MjMzYjFkODQ4MjM4ZjMxIiwiY3JlYXRlZF9hdCI6MTY3NDIwNDUzNSwia2luZCI6OTczNSwidGFncyI6W1sicCIsIjMyZTE4Mjc2MzU0NTBlYmIzYzVhN2QxMmMxZjhlN2IyYjUxNDQzOWFjMTBhNjdlZWYzZDlmZDljNWM2OGUyNDUiXSxbImJvbHQxMSIsImxuYmMxMHUxcDN1NTR0bnNwNTcyOXF2eG5renRqamtkNTg1eW4wbDg2MzBzMm01eDZsNTZ3eXk0ZWMybnU4eHV6NjI5eHFwcDV2MnE3aHVjNGpwamgwM2Z4OHVqZXQ1Nms3OWd4cXg3bWUycGV2ejZqMms4dDhtNGxnNXZxaHA1eWc1MDU3OGNtdWoyNG1mdDNxcnNybWd3ZjMwa2U3YXY3ZDc3Z2FtZmxkazlrNHNmMzltcXhxeWp3NXFjcXBqcnpqcTJoeWVoNXEzNmx3eDZ6dHd5cmw2dm1tcnZ6NnJ1ZndqZnI4N3lremZuYXR1a200dWRzNHl6YWszc3FxOW1jcXFxcXFxcWxncXFxcTg2cXF5ZzlxeHBxeXNncWFkeWVjdmR6ZjI3MHBkMzZyc2FmbDA3azQ1ZmNqMnN5OGU1djJ0ZW5kNTB2OTU3NnV4cDNkdmp6amV1aHJlODl5cGdjbTkwZDZsbTAwNGszMHlqNGF2NW1jc3M1bnl4NHU5bmVyOWdwcHY2eXF3Il0sWyJkZXNjcmlwdGlvbiIsIntcImlkXCI6XCJiMDkyMTYzNGIxYmI4ZWUzNTg0YmJiZjJlOGQ3OTBhZDk4NTk5ZDhlMDhmODFjNzAwZGRiZTQ4MjAxNTY4Yjk3XCIsXCJwdWJrZXlcIjpcIjdmYTU2ZjVkNjk2MmFiMWUzY2Q0MjRlNzU4YzMwMDJiODY2NWY3YjBkOGRjZWU5ZmU5ZTI4OGQ3NzUxYWMxOTRcIixcImNyZWF0ZWRfYXRcIjoxNjc0MjA0NTMxLFwia2luZFwiOjk3MzQsXCJ0YWdzXCI6W1tcInBcIixcIjMyZTE4Mjc2MzU0NTBlYmIzYzVhN2QxMmMxZjhlN2IyYjUxNDQzOWFjMTBhNjdlZWYzZDlmZDljNWM2OGUyNDVcIl0sW1wicmVsYXlzXCIsXCJ3c3M6Ly9yZWxheS5zbm9ydC5zb2NpYWxcIixcIndzczovL3JlbGF5LmRhbXVzLmlvXCIsXCJ3c3M6Ly9ub3N0ci1wdWIud2VsbG9yZGVyLm5ldFwiLFwid3NzOi8vbm9zdHIudjBsLmlvXCIsXCJ3c3M6Ly9wcml2YXRlLW5vc3RyLnYwbC5pb1wiLFwid3NzOi8vbm9zdHIuemViZWRlZS5jbG91ZFwiLFwid3NzOi8vcmVsYXkubm9zdHIuaW5mby9cIl1dLFwiY29udGVudFwiOlwiXCIsXCJzaWdcIjpcImQwODQwNGU2MjVmOWM1NjMzYWZhZGQxMWMxMTBiYTg4ZmNkYjRiOWUwOTJiOTg0MGU3NDgyYThkNTM3YjFmYzExODY5MmNmZDEzMWRkODMzNTM2NDc2OWE2NzE3NTRhZDdhYTk3MzEzNjgzYTRhZDdlZmI3NjQ3NmMwNGU1ZjE3XCJ9Il0sWyJwcmVpbWFnZSIsIjNlMDJhM2FmOGM4YmNmMmEzNzUzYzg3ZjMxMTJjNjU2YTIwMTE0ZWUwZTk4ZDgyMTliYzU2ZjVlOGE3MjM1YjMiXV0sImNvbnRlbnQiOiIiLCJzaWciOiIzYWI0NGQwZTIyMjhiYmQ0ZDIzNDFjM2ZhNzQwOTZjZmY2ZjU1Y2ZkYTk5YTVkYWRjY2Y0NWM2NjQ2MzdlMjExNTFiMmY5ZGQwMDQwZjFhMjRlOWY4Njg2NzM4YjE2YmY4MTM0YmRiZTQxYTIxOGM5MTFmN2JiMzFlNTk1NzhkMSJ9Cg==" let zapjson = "eyJpZCI6IjUzNmJlZTllODNjODE4ZTNiODJjMTAxOTM1MTI4YWUyN2EwZDQyOTAwMzlhYWYyNTNlZmU1ZjA5MjMyYzE5NjIiLCJwdWJrZXkiOiI5NjMwZjQ2NGNjYTZhNTE0N2FhOGEzNWYwYmNkZDNjZTQ4NTMyNGU3MzJmZDM5ZTA5MjMzYjFkODQ4MjM4ZjMxIiwiY3JlYXRlZF9hdCI6MTY3NDIwNDUzNSwia2luZCI6OTczNSwidGFncyI6W1sicCIsIjMyZTE4Mjc2MzU0NTBlYmIzYzVhN2QxMmMxZjhlN2IyYjUxNDQzOWFjMTBhNjdlZWYzZDlmZDljNWM2OGUyNDUiXSxbImJvbHQxMSIsImxuYmMxMHUxcDN1NTR0bnNwNTcyOXF2eG5renRqamtkNTg1eW4wbDg2MzBzMm01eDZsNTZ3eXk0ZWMybnU4eHV6NjI5eHFwcDV2MnE3aHVjNGpwamgwM2Z4OHVqZXQ1Nms3OWd4cXg3bWUycGV2ejZqMms4dDhtNGxnNXZxaHA1eWc1MDU3OGNtdWoyNG1mdDNxcnNybWd3ZjMwa2U3YXY3ZDc3Z2FtZmxkazlrNHNmMzltcXhxeWp3NXFjcXBqcnpqcTJoeWVoNXEzNmx3eDZ6dHd5cmw2dm1tcnZ6NnJ1ZndqZnI4N3lremZuYXR1a200dWRzNHl6YWszc3FxOW1jcXFxcXFxcWxncXFxcTg2cXF5ZzlxeHBxeXNncWFkeWVjdmR6ZjI3MHBkMzZyc2FmbDA3azQ1ZmNqMnN5OGU1djJ0ZW5kNTB2OTU3NnV4cDNkdmp6amV1aHJlODl5cGdjbTkwZDZsbTAwNGszMHlqNGF2NW1jc3M1bnl4NHU5bmVyOWdwcHY2eXF3Il0sWyJkZXNjcmlwdGlvbiIsIntcImlkXCI6XCJiMDkyMTYzNGIxYmI4ZWUzNTg0YmJiZjJlOGQ3OTBhZDk4NTk5ZDhlMDhmODFjNzAwZGRiZTQ4MjAxNTY4Yjk3XCIsXCJwdWJrZXlcIjpcIjdmYTU2ZjVkNjk2MmFiMWUzY2Q0MjRlNzU4YzMwMDJiODY2NWY3YjBkOGRjZWU5ZmU5ZTI4OGQ3NzUxYWMxOTRcIixcImNyZWF0ZWRfYXRcIjoxNjc0MjA0NTMxLFwia2luZFwiOjk3MzQsXCJ0YWdzXCI6W1tcInBcIixcIjMyZTE4Mjc2MzU0NTBlYmIzYzVhN2QxMmMxZjhlN2IyYjUxNDQzOWFjMTBhNjdlZWYzZDlmZDljNWM2OGUyNDVcIl0sW1wicmVsYXlzXCIsXCJ3c3M6Ly9yZWxheS5zbm9ydC5zb2NpYWxcIixcIndzczovL3JlbGF5LmRhbXVzLmlvXCIsXCJ3c3M6Ly9ub3N0ci1wdWIud2VsbG9yZGVyLm5ldFwiLFwid3NzOi8vbm9zdHIudjBsLmlvXCIsXCJ3c3M6Ly9wcml2YXRlLW5vc3RyLnYwbC5pb1wiLFwid3NzOi8vbm9zdHIuemViZWRlZS5jbG91ZFwiLFwid3NzOi8vcmVsYXkubm9zdHIuaW5mby9cIl1dLFwiY29udGVudFwiOlwiXCIsXCJzaWdcIjpcImQwODQwNGU2MjVmOWM1NjMzYWZhZGQxMWMxMTBiYTg4ZmNkYjRiOWUwOTJiOTg0MGU3NDgyYThkNTM3YjFmYzExODY5MmNmZDEzMWRkODMzNTM2NDc2OWE2NzE3NTRhZDdhYTk3MzEzNjgzYTRhZDdlZmI3NjQ3NmMwNGU1ZjE3XCJ9Il0sWyJwcmVpbWFnZSIsIjNlMDJhM2FmOGM4YmNmMmEzNzUzYzg3ZjMxMTJjNjU2YTIwMTE0ZWUwZTk4ZDgyMTliYzU2ZjVlOGE3MjM1YjMiXV0sImNvbnRlbnQiOiIiLCJzaWciOiIzYWI0NGQwZTIyMjhiYmQ0ZDIzNDFjM2ZhNzQwOTZjZmY2ZjU1Y2ZkYTk5YTVkYWRjY2Y0NWM2NjQ2MzdlMjExNTFiMmY5ZGQwMDQwZjFhMjRlOWY4Njg2NzM4YjE2YmY4MTM0YmRiZTQxYTIxOGM5MTFmN2JiMzFlNTk1NzhkMSJ9Cg=="