Files
damus/damus/Shared/Utilities/Bech32Object.swift
William Casarin f8185d0ca5 fixes
Signed-off-by: William Casarin <jb55@jb55.com>
2025-08-11 16:40:01 -07:00

323 lines
12 KiB
Swift

//
// Bech32Object.swift
// damus
//
// Created by William Casarin on 2023-01-28.
//
import Foundation
fileprivate extension String {
/// Failable initializer to build a Swift.String from a C-backed `str_block_t`.
init?(_ s: str_block_t) {
let bytes = Data(bytes: s.str, count: Int(s.len))
self.init(bytes: bytes, encoding: .utf8)
}
}
struct NEvent : Equatable, Hashable {
let noteid: NoteId
let relays: [RelayURL]
let author: Pubkey?
let kind: UInt32?
init(noteid: NoteId, relays: [RelayURL], author: Pubkey? = nil, kind: UInt32? = nil) {
self.noteid = noteid
self.relays = relays
self.author = author
self.kind = kind
}
init(event: NostrEvent, relays: [RelayURL]) {
self.init(noteid: event.id, relays: relays, author: event.pubkey, kind: event.kind)
}
}
struct NProfile : Equatable, Hashable {
let author: Pubkey
let relays: [RelayURL]
}
struct NAddr : Equatable, Hashable {
let identifier: String
let author: Pubkey
let relays: [RelayURL]
let kind: UInt32
}
extension ndb_relays {
func as_urls() -> [RelayURL] {
var urls = [RelayURL]()
//
// This is so incredibly dumb but it's just what the Swift <-> C bridge
// does and I don't have a better way that doesn't involve complicated
// and slow stuff like reflection
//
let (r0,r1,r2,r3,r4,r5,r6,r7,r8,r9,r10,r11,r12,r13,r14,r15,r16,r17,r18,r19,r20,r21,r22,r23) = self.relays
for i in 0..<self.num_relays {
switch i {
case 0: if let relay = RelayURL(r0.as_str()) { urls.append(relay) }
case 1: if let relay = RelayURL(r1.as_str()) { urls.append(relay) }
case 2: if let relay = RelayURL(r2.as_str()) { urls.append(relay) }
case 3: if let relay = RelayURL(r3.as_str()) { urls.append(relay) }
case 4: if let relay = RelayURL(r4.as_str()) { urls.append(relay) }
case 5: if let relay = RelayURL(r5.as_str()) { urls.append(relay) }
case 6: if let relay = RelayURL(r6.as_str()) { urls.append(relay) }
case 7: if let relay = RelayURL(r7.as_str()) { urls.append(relay) }
case 8: if let relay = RelayURL(r8.as_str()) { urls.append(relay) }
case 9: if let relay = RelayURL(r9.as_str()) { urls.append(relay) }
case 10: if let relay = RelayURL(r10.as_str()) { urls.append(relay) }
case 11: if let relay = RelayURL(r11.as_str()) { urls.append(relay) }
case 12: if let relay = RelayURL(r12.as_str()) { urls.append(relay) }
case 13: if let relay = RelayURL(r13.as_str()) { urls.append(relay) }
case 14: if let relay = RelayURL(r14.as_str()) { urls.append(relay) }
case 15: if let relay = RelayURL(r15.as_str()) { urls.append(relay) }
case 16: if let relay = RelayURL(r16.as_str()) { urls.append(relay) }
case 17: if let relay = RelayURL(r17.as_str()) { urls.append(relay) }
case 18: if let relay = RelayURL(r18.as_str()) { urls.append(relay) }
case 19: if let relay = RelayURL(r19.as_str()) { urls.append(relay) }
case 20: if let relay = RelayURL(r20.as_str()) { urls.append(relay) }
case 21: if let relay = RelayURL(r21.as_str()) { urls.append(relay) }
case 22: if let relay = RelayURL(r22.as_str()) { urls.append(relay) }
case 23: if let relay = RelayURL(r23.as_str()) { urls.append(relay) }
default:
break
}
}
return urls
}
}
enum Bech32Object : Equatable, Hashable {
case nsec(Privkey)
case npub(Pubkey)
case note(NoteId)
case nscript([UInt8])
case nevent(NEvent)
case nprofile(NProfile)
case nrelay(String)
case naddr(NAddr)
func pubkey() -> Pubkey? {
switch self {
case .nprofile(let nprofile): return nprofile.author
case .npub(let pubkey): return pubkey
case .nevent(let ev): return ev.author
case .naddr(let naddr): return naddr.author
case .nscript: return nil
case .nsec: return nil // TODO privkey_to_pubkey ?
case .note: return nil
case .nrelay: return nil
}
}
init?(block: ndb_mention_bech32_block) {
let b32 = block.bech32
switch block.bech32_type {
case .note:
let data = b32.note.event_id.as_data(size: 32)
self = .note(NoteId(data))
case .npub:
let data = b32.npub.pubkey.as_data(size: 32)
self = .npub(Pubkey(data))
case .nprofile:
let pk = b32.nprofile.pubkey.as_data(size: 32)
let relays = b32.nprofile.relays.as_urls()
self = .nprofile(NProfile(author: Pubkey(pk), relays: relays))
case .nevent:
let nevent = b32.nevent
let note_id = NoteId(nevent.event_id.as_data(size: 32))
let relays = nevent.relays.as_urls()
var author: Pubkey? = nil
if nevent.pubkey != nil {
author = Pubkey(nevent.pubkey.as_data(size: 32))
}
var kind: UInt32? = nil
if nevent.has_kind {
kind = nevent.kind
}
self = .nevent(NEvent(noteid: note_id, relays: relays, author: author, kind: kind))
case .nrelay:
self = .nrelay(b32.nrelay.relay.as_str())
case .naddr:
let identifier = b32.naddr.identifier.as_str()
let author = Pubkey(b32.naddr.pubkey.as_data(size: 32))
let relays = b32.naddr.relays.as_urls()
self = .naddr(NAddr(identifier: identifier, author: author, relays: relays, kind: b32.naddr.kind))
case .nsec:
return nil
case .none:
return nil
}
}
static func parse(_ str: String) -> Bech32Object? {
if str.starts(with: "nscript"), let decoded = try? bech32_decode(str) {
return .nscript(decoded.data.bytes)
}
var b: nostr_bech32_t = nostr_bech32()
var bytes = Data(capacity: str.utf8.count)
let ok = str.withCString { cstr in
let ok = bytes.withUnsafeMutableBytes { buffer -> Int32 in
guard let addr = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return 0
}
return parse_nostr_bech32(addr, Int32(buffer.count), cstr, str.utf8.count, &b)
}
return ok != 0
}
guard ok else { return nil }
return decodeCBech32(b)
}
static func encode(_ obj: Bech32Object) -> String {
switch(obj) {
case .note(let noteid):
return bech32_encode(hrp: "note", noteid.bytes)
case .nevent(let nevent): return bech32EncodeNevent(nevent)
case .nprofile(let nprofile): return bech32EncodeNprofile(nprofile)
case .nrelay(let relayURL): return bech32EncodeNrelay(relayURL: relayURL)
case .naddr(let naddr): return bech32EncodeNaddr(naddr)
case .npub(let pubkey):
return bech32_encode(hrp: "npub", pubkey.bytes)
case .nsec(let privkey):
guard let pubkey = privkey_to_pubkey(privkey: privkey) else { return "" }
return bech32_encode(hrp: "npub", pubkey.bytes)
case .nscript(let data):
return bech32_encode(hrp: "nscript", data)
}
}
func toMentionRef() -> MentionRef? {
MentionRef(nip19: self)
}
}
func decodeCBech32(_ b: nostr_bech32_t) -> Bech32Object? {
switch b.type {
case NOSTR_BECH32_NOTE:
let note_id = NoteId(Data(bytes: b.note.event_id, count: 32))
return .note(note_id)
case NOSTR_BECH32_NEVENT:
let note_id = NoteId(Data(bytes: b.nevent.event_id, count: 32))
let pubkey = b.nevent.pubkey != nil ? Pubkey(Data(bytes: b.nevent.pubkey, count: 32)) : nil
let kind: UInt32? = !b.nevent.has_kind ? nil : b.nevent.kind
let relays = b.nevent.relays.as_urls()
return .nevent(NEvent(noteid: note_id, relays: relays, author: pubkey, kind: kind))
case NOSTR_BECH32_NPUB:
let pubkey = Pubkey(Data(bytes: b.npub.pubkey, count: 32))
return .npub(pubkey)
case NOSTR_BECH32_NSEC:
let privkey = Privkey(Data(bytes: b.nsec.nsec, count: 32))
guard let pubkey = privkey_to_pubkey(privkey: privkey) else { return nil }
return .npub(pubkey)
case NOSTR_BECH32_NPROFILE:
let pubkey = Pubkey(Data(bytes: b.nprofile.pubkey, count: 32))
return .nprofile(NProfile(author: pubkey, relays: b.nprofile.relays.as_urls()))
case NOSTR_BECH32_NRELAY:
return .nrelay(b.nrelay.relay.as_str())
case NOSTR_BECH32_NADDR:
let pubkey = Pubkey(Data(bytes: b.naddr.pubkey, count: 32))
let kind = b.naddr.kind
let identifier = b.naddr.identifier.as_str()
return .naddr(NAddr(identifier: identifier, author: pubkey, relays: b.naddr.relays.as_urls(), kind: kind))
default:
return nil
}
}
private enum TLVType: UInt8 {
case SPECIAL
case RELAY
case AUTHOR
case KIND
}
private func writeBytesList(bytesList: inout [UInt8], tlvType: TLVType, data: [UInt8]){
bytesList.append(tlvType.rawValue)
bytesList.append(UInt8(data.bytes.count))
bytesList.append(contentsOf: data.bytes)
}
private func writeBytesRelays(bytesList: inout [UInt8], relays: [RelayURL]) {
for relay in relays {
guard let relayData = relay.url.absoluteString.data(using: .utf8) else {
continue // skip relay if can't read data
}
writeBytesList(bytesList: &bytesList, tlvType: .RELAY, data: relayData.bytes)
}
}
private func writeBytesKind(bytesList: inout [UInt8], kind: UInt32) {
bytesList.append(TLVType.KIND.rawValue)
bytesList.append(UInt8(4))
var bigEndianBytes = kind.bigEndian
let data = Data(bytes: &bigEndianBytes, count: MemoryLayout<UInt32>.size)
bytesList.append(contentsOf: data)
}
private func bech32EncodeNevent(_ nevent: NEvent) -> String {
var neventBytes = [UInt8]();
writeBytesList(bytesList: &neventBytes, tlvType: .SPECIAL, data: nevent.noteid.bytes)
writeBytesRelays(bytesList: &neventBytes, relays: nevent.relays)
if let eventPubkey = nevent.author {
writeBytesList(bytesList: &neventBytes, tlvType: .AUTHOR, data: eventPubkey.bytes)
}
if let kind = nevent.kind {
writeBytesKind(bytesList: &neventBytes, kind: kind)
}
return bech32_encode(hrp: "nevent", neventBytes.bytes)
}
private func bech32EncodeNprofile(_ nprofile: NProfile) -> String {
var nprofileBytes = [UInt8]();
writeBytesList(bytesList: &nprofileBytes, tlvType: .SPECIAL, data: nprofile.author.bytes)
writeBytesRelays(bytesList: &nprofileBytes, relays: nprofile.relays)
return bech32_encode(hrp: "nprofile", nprofileBytes.bytes)
}
private func bech32EncodeNrelay(relayURL: String) -> String {
var nrelayBytes = [UInt8]();
guard let relayURLBytes = relayURL.data(using: .ascii) else {
return ""
}
writeBytesList(bytesList: &nrelayBytes, tlvType: .SPECIAL, data: relayURLBytes.bytes)
return bech32_encode(hrp: "nrelay", nrelayBytes.bytes)
}
private func bech32EncodeNaddr(_ naddr: NAddr) -> String {
var naddrBytes = [UInt8]();
guard let identifierBytes = naddr.identifier.data(using: .utf8) else {
return ""
}
writeBytesList(bytesList: &naddrBytes, tlvType: .SPECIAL, data: identifierBytes.bytes)
writeBytesRelays(bytesList: &naddrBytes, relays: naddr.relays)
writeBytesList(bytesList: &naddrBytes, tlvType: .AUTHOR, data: naddr.author.bytes)
writeBytesKind(bytesList: &naddrBytes, kind: naddr.kind)
return bech32_encode(hrp: "naddr", naddrBytes.bytes)
}