yysskk/swift-nostr-client
A modern Swift library for the Nostr protocol, built with Swift 6 concurrency support.
Features
- Full NIP-01 Support: Events, subscriptions, and relay communication
- NIP-02 Contact List: Follow/unfollow users and manage contact lists
- NIP-03 OpenTimestamps: Attach OTS attestations to events
- NIP-05 Verification: DNS-based identifier verification
- NIP-06 Key Derivation: Generate keys from BIP-39 mnemonic seed phrases
- NIP-17 Private DMs: End-to-end encrypted direct messages with sender anonymity
- Cryptographic Operations: Schnorr signatures with secp256k1
- Bech32 Encoding: npub/nsec key encoding (NIP-19)
- Async/Await: Modern Swift concurrency with actors
- Multi-Relay Support: Connect to multiple relays with RelayPool
- Type-Safe: Full Sendable compliance for thread safety
Requirements
- Swift 6.2+
- iOS 17.0+ / macOS 14.0+ / tvOS 17.0+ / watchOS 10.0+ / visionOS 1.0+
Installation
Swift Package Manager
Add to your Package.swift:
dependencies: [
.package(url: "https://github.com/yysskk/swift-nostr-client", from: "1.0.0")
]Or add via Xcode: File → Add Package Dependencies → Enter repository URL.
Quick Start
Generate Keys
import NostrClient
// Generate a new random keypair
let keyPair = try KeyPair()
print("Public Key: \(keyPair.publicKeyHex)")
print("npub: \(keyPair.npub)")
print("nsec: \(keyPair.nsec)")
// Import from nsec
let imported = try KeyPair(nsec: "nsec1...")
// Generate from mnemonic (NIP-06)
let (mnemonic, keyPairFromMnemonic) = try KeyPair.generate(wordCount: 12)
print("Mnemonic: \(mnemonic.phrase)")
print("Public Key: \(keyPairFromMnemonic.npub)")
// Restore from existing mnemonic
let restored = try KeyPair(mnemonicPhrase: "leader monkey parrot ring guide accident before fence cannon height naive bean")Connect to Relays
let client = NostrClient()
// Add relays
try await client.addRelays([
"wss://relay.damus.io",
"wss://nos.lol",
"wss://relay.nostr.band"
])
// Connect
try await client.connect()Publish Events
// Set your private key
try await client.setNsec("nsec1...")
// Publish a text note
let event = try await client.publishTextNote(content: "Hello, Nostr!")
// Publish with tags
let tagged = try await client.publishTextNote(
content: "Check out #nostr",
tags: [["t", "nostr"]]
)
// React to an event
try await client.publishReaction(to: event, content: "🤙")Subscribe to Events
// Subscribe to a user's notes
let subscriptionId = try await client.subscribeToUserTimeline(pubkey: "...") { event in
print("New note: \(event.content)")
}
// Subscribe to the global feed
try await client.subscribeToGlobalFeed(limit: 50) { event in
print("Global: \(event.content)")
}
// Custom filter subscription
let filter = Filter(
kinds: [1],
authors: ["pubkey1", "pubkey2"],
limit: 100
)
try await client.subscribe(filters: [filter]) { event in
print("Received: \(event.id)")
}
// Unsubscribe
try await client.unsubscribe(subscriptionId: subscriptionId)Fetch Events
// Fetch specific event
let event = try await client.fetchEvent(id: "eventid...")
// Fetch user metadata
let metadata = try await client.fetchMetadata(pubkey: "...")
print("Name: \(metadata?.name ?? "Unknown")")Models
Event
public struct Event: Codable, Identifiable, Hashable, Sendable {
public let id: String
public let pubkey: String
public let createdAt: Int64
public let kind: Int
public let tags: [[String]]
public let content: String
public let sig: String
}Filter
public struct Filter: Codable, Sendable, Hashable {
public var ids: [String]?
public var authors: [String]?
public var kinds: [Int]?
public var eventReferences: [String]? // #e
public var pubkeyReferences: [String]? // #p
public var since: Int64?
public var until: Int64?
public var limit: Int?
}Event Kinds
Event.Kind.setMetadata // 0
Event.Kind.textNote // 1
Event.Kind.recommendRelay // 2
Event.Kind.contacts // 3
Event.Kind.eventDeletion // 5
Event.Kind.repost // 6
Event.Kind.reaction // 7
Event.Kind.seal // 13 (NIP-59)
Event.Kind.privateDirectMessage // 14 (NIP-17)
Event.Kind.giftWrap // 1059 (NIP-59)
Event.Kind.zapRequest // 9734
Event.Kind.zap // 9735
// ... and moreLow-Level API
Direct Relay Connection
let connection = try RelayConnection(urlString: "wss://relay.damus.io")
await connection.connect()
// Subscribe
try await connection.subscribe(
subscriptionId: "sub1",
filters: [Filter(kinds: [1], limit: 10)]
)
// Listen for messages
for await message in await connection.messages() {
switch message {
case .event(let subId, let event):
print("Event: \(event.content)")
case .endOfStoredEvents(let subId):
print("EOSE for \(subId)")
case .notice(let msg):
print("Notice: \(msg)")
default:
break
}
}Manual Event Signing
let keyPair = try KeyPair()
let signer = EventSigner(keyPair: keyPair)
let unsigned = UnsignedEvent(
pubkey: keyPair.publicKeyHex,
kind: .textNote,
tags: [["t", "test"]],
content: "Manual signing example"
)
let signed = try signer.sign(unsigned)
// Verify signature
let isValid = try signed.verify()Supported NIPs
- [x] NIP-01: Basic protocol
- [x] NIP-02: Contact list and petnames
- [x] NIP-03: OpenTimestamps attestations
- [x] NIP-05: DNS-based identifiers
- [x] NIP-06: Basic key derivation from mnemonic seed phrase
- [x] NIP-17: Private direct messages
- [x] NIP-19: bech32-encoded entities (npub, nsec)
- [x] NIP-20: Command Results (OK)
- [x] NIP-42: Authentication (AUTH message parsing)
- [x] NIP-44: Versioned encryption
- [x] NIP-59: Gift wrap
License
MIT License
Package Metadata
Repository: yysskk/swift-nostr-client
Default branch: main
README: README.md