damus

nostr ios client
git clone git://jb55.com/damus
Log | Files | Refs | README | LICENSE

commit cb4adf06f12bee149367829f9b2f0f7278593651
parent 97fc415b8c0287105e89fea813898d3a1aca2178
Author: kernelkind <kernelkind@gmail.com>
Date:   Sat, 13 Jan 2024 14:19:44 -0500

nip19: added swift enums

Add enums to reflect Bech32 with TLV encoded data. Update parse method
to call C library for generalized parsing of bech32 data.

Lightning-url: LNURL1DP68GURN8GHJ7EM9W3SKCCNE9E3K7MF0D3H82UNVWQHKWUN9V4HXGCTHDC6RZVGR8SW3G
Signed-off-by: kernelkind <kernelkind@gmail.com>
Reviewed-by: William Casarin <jb55@jb55.com>
Signed-off-by: William Casarin <jb55@jb55.com>

Diffstat:
Mdamus.xcodeproj/project.pbxproj | 8++++++--
Mdamus/Nostr/NostrLink.swift | 43+++++++++++++++++++------------------------
Mdamus/Util/Bech32Object.swift | 152+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
AdamusTests/Bech32ObjectTests.swift | 118+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 284 insertions(+), 37 deletions(-)

diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj @@ -260,8 +260,8 @@ 4C9B0DF32A65C46800CBDA21 /* ProfileEditButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9B0DF22A65C46800CBDA21 /* ProfileEditButton.swift */; }; 4C9BB83129C0ED4F00FC4E37 /* DisplayName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9BB83029C0ED4F00FC4E37 /* DisplayName.swift */; }; 4C9BB83429C12D9900FC4E37 /* EventProfileName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9BB83329C12D9900FC4E37 /* EventProfileName.swift */; }; - 4C9D6D1B2B1D35D7004E5CD9 /* PullDownSearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9D6D1A2B1D35D7004E5CD9 /* PullDownSearch.swift */; }; 4C9D6D162B1AA9C6004E5CD9 /* DisplayTabBarNotify.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9D6D152B1AA9C6004E5CD9 /* DisplayTabBarNotify.swift */; }; + 4C9D6D1B2B1D35D7004E5CD9 /* PullDownSearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9D6D1A2B1D35D7004E5CD9 /* PullDownSearch.swift */; }; 4C9F18E229AA9B6C008C55EC /* CustomizeZapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9F18E129AA9B6C008C55EC /* CustomizeZapView.swift */; }; 4C9F18E429ABDE6D008C55EC /* MaybeAnonPfpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9F18E329ABDE6D008C55EC /* MaybeAnonPfpView.swift */; }; 4CA2EFA0280E37AC0044ACD8 /* TimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA2EF9F280E37AC0044ACD8 /* TimelineView.swift */; }; @@ -610,6 +610,7 @@ D7EDED342B12ACAE0018B19C /* DamusUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7EDED322B12ACAE0018B19C /* DamusUserDefaults.swift */; }; D7FB10A72B0C371A00FA8D42 /* Log.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C2B10272A7B0F5C008AA43E /* Log.swift */; }; D7FF94002AC7AC5300FD969D /* RelayURL.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7FF93FF2AC7AC5200FD969D /* RelayURL.swift */; }; + E02B54182B4DFADA0077FF42 /* Bech32ObjectTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E02B54172B4DFADA0077FF42 /* Bech32ObjectTests.swift */; }; E4FA1C032A24BB7F00482697 /* SearchSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4FA1C022A24BB7F00482697 /* SearchSettingsView.swift */; }; E990020F2955F837003BBC5A /* EditMetadataView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E990020E2955F837003BBC5A /* EditMetadataView.swift */; }; E9E4ED0B295867B900DD7078 /* ThreadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E4ED0A295867B900DD7078 /* ThreadView.swift */; }; @@ -1135,8 +1136,8 @@ 4C9B0DF22A65C46800CBDA21 /* ProfileEditButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileEditButton.swift; sourceTree = "<group>"; }; 4C9BB83029C0ED4F00FC4E37 /* DisplayName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayName.swift; sourceTree = "<group>"; }; 4C9BB83329C12D9900FC4E37 /* EventProfileName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventProfileName.swift; sourceTree = "<group>"; }; - 4C9D6D1A2B1D35D7004E5CD9 /* PullDownSearch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PullDownSearch.swift; sourceTree = "<group>"; }; 4C9D6D152B1AA9C6004E5CD9 /* DisplayTabBarNotify.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayTabBarNotify.swift; sourceTree = "<group>"; }; + 4C9D6D1A2B1D35D7004E5CD9 /* PullDownSearch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PullDownSearch.swift; sourceTree = "<group>"; }; 4C9F18E129AA9B6C008C55EC /* CustomizeZapView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomizeZapView.swift; sourceTree = "<group>"; }; 4C9F18E329ABDE6D008C55EC /* MaybeAnonPfpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaybeAnonPfpView.swift; sourceTree = "<group>"; }; 4CA2EF9F280E37AC0044ACD8 /* TimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineView.swift; sourceTree = "<group>"; }; @@ -1369,6 +1370,7 @@ D7EDED2D2B128E8A0018B19C /* CollectionExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionExtension.swift; sourceTree = "<group>"; }; D7EDED322B12ACAE0018B19C /* DamusUserDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusUserDefaults.swift; sourceTree = "<group>"; }; D7FF93FF2AC7AC5200FD969D /* RelayURL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayURL.swift; sourceTree = "<group>"; }; + E02B54172B4DFADA0077FF42 /* Bech32ObjectTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Bech32ObjectTests.swift; sourceTree = "<group>"; }; E4FA1C022A24BB7F00482697 /* SearchSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchSettingsView.swift; sourceTree = "<group>"; }; E990020E2955F837003BBC5A /* EditMetadataView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditMetadataView.swift; sourceTree = "<group>"; }; E9E4ED0A295867B900DD7078 /* ThreadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadView.swift; sourceTree = "<group>"; }; @@ -2440,6 +2442,7 @@ F944F56C29EA9CB20067B3BF /* Models */, 50A50A8C29A09E1C00C01BE7 /* RequestTests.swift */, 4C90BD1B283AC38E008EE7EF /* Bech32Tests.swift */, + E02B54172B4DFADA0077FF42 /* Bech32ObjectTests.swift */, 4C363A9F2828A8DD006E126D /* LikeTests.swift */, 4C363A9D2828A822006E126D /* ReplyTests.swift */, 4CE6DEF727F7A08200C66700 /* damusTests.swift */, @@ -3383,6 +3386,7 @@ 3A3040F329A91366008A0F29 /* ProfileViewTests.swift in Sources */, 4CF0ABDC2981A19E00D66079 /* ListTests.swift in Sources */, 4C684A552A7E91FE005E6031 /* LongPostTests.swift in Sources */, + E02B54182B4DFADA0077FF42 /* Bech32ObjectTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/damus/Nostr/NostrLink.swift b/damus/Nostr/NostrLink.swift @@ -60,20 +60,7 @@ func decode_universal_link(_ s: String) -> NostrLink? { uri = uri.replacingOccurrences(of: "https://damus.io/", with: "") uri = uri.replacingOccurrences(of: "/", with: "") - guard let decoded = try? bech32_decode(uri), - decoded.data.count == 32 - else { - return nil - } - - if decoded.hrp == "note" { - return .ref(.event(NoteId(decoded.data))) - } else if decoded.hrp == "npub" { - return .ref(.pubkey(Pubkey(decoded.data))) - } - // TODO: handle nprofile, etc - - return nil + return decode_nostr_bech32_uri(uri) } func decode_nostr_bech32_uri(_ s: String) -> NostrLink? { @@ -82,16 +69,24 @@ func decode_nostr_bech32_uri(_ s: String) -> NostrLink? { } switch obj { - case .nsec(let privkey): - guard let pubkey = privkey_to_pubkey(privkey: privkey) else { return nil } - return .ref(.pubkey(pubkey)) - case .npub(let pubkey): - return .ref(.pubkey(pubkey)) - case .note(let id): - return .ref(.event(id)) - case .nscript(let data): - return .script(data) - } + case .nsec(let privkey): + guard let pubkey = privkey_to_pubkey(privkey: privkey) else { return nil } + return .ref(.pubkey(pubkey)) + case .npub(let pubkey): + return .ref(.pubkey(pubkey)) + case .note(let id): + return .ref(.event(id)) + case .nscript(let data): + return .script(data) + case .naddr(let naddr): + return .none // TODO: FIX + case .nevent(let nevent): + return .ref(.event(nevent.noteid)) + case .nprofile(let nprofile): + return .ref(.pubkey(nprofile.author)) + case .nrelay(_): + return .none + } } func decode_nostr_uri(_ s: String) -> NostrLink? { diff --git a/damus/Util/Bech32Object.swift b/damus/Util/Bech32Object.swift @@ -7,28 +7,158 @@ 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 len = s.end - s.start + let bytes = Data(bytes: s.start, count: len) + self.init(bytes: bytes, encoding: .utf8) + } +} -enum Bech32Object { +struct NEvent : Equatable { + let noteid: NoteId + let relays: [String] + let author: Pubkey? + let kind: UInt32? + + init(noteid: NoteId, relays: [String]) { + self.noteid = noteid + self.relays = relays + self.author = nil + self.kind = nil + } + + init(noteid: NoteId, relays: [String], author: Pubkey?) { + self.noteid = noteid + self.relays = relays + self.author = author + self.kind = nil + } + init(noteid: NoteId, relays: [String], kind: UInt32?) { + self.noteid = noteid + self.relays = relays + self.author = nil + self.kind = kind + } + init(noteid: NoteId, relays: [String], author: Pubkey?, kind: UInt32?) { + self.noteid = noteid + self.relays = relays + self.author = author + self.kind = kind + } +} + +struct NProfile : Equatable { + let author: Pubkey + let relays: [String] +} + +struct NAddr : Equatable { + let identifier: String + let author: Pubkey + let relays: [String] + let kind: UInt32 +} + +enum Bech32Object : Equatable { case nsec(Privkey) case npub(Pubkey) case note(NoteId) case nscript([UInt8]) + case nevent(NEvent) + case nprofile(NProfile) + case nrelay(String) + case naddr(NAddr) static func parse(_ str: String) -> Bech32Object? { - guard let decoded = try? bech32_decode(str) else { - return nil + var b: nostr_bech32_t = nostr_bech32() + + let bytes = Array(str.utf8) + + bytes.withUnsafeBufferPointer { buffer in + guard let baseAddress = buffer.baseAddress else { return } + + var cursorInstance = cursor() + cursorInstance.start = UnsafeMutablePointer(mutating: baseAddress) + cursorInstance.p = UnsafeMutablePointer(mutating: baseAddress) + cursorInstance.end = cursorInstance.start.advanced(by: buffer.count) + + parse_nostr_bech32(&cursorInstance, &b) } - if decoded.hrp == "npub" { - return .npub(Pubkey(decoded.data)) - } else if decoded.hrp == "nsec" { - return .nsec(Privkey(decoded.data)) - } else if decoded.hrp == "note" { - return .note(NoteId(decoded.data)) - } else if decoded.hrp == "nscript" { - return .nscript(decoded.data.bytes) + return decodeCBech32(b) + } +} + +func decodeCBech32(_ b: nostr_bech32_t) -> Bech32Object? { + switch b.type { + case NOSTR_BECH32_NOTE: + let note = b.data.note; + let note_id = NoteId(Data(bytes: note.event_id, count: 32)) + return .note(note_id) + case NOSTR_BECH32_NEVENT: + let nevent = b.data.nevent; + let note_id = NoteId(Data(bytes: nevent.event_id, count: 32)) + let pubkey = nevent.pubkey != nil ? Pubkey(Data(bytes: nevent.pubkey, count: 32)) : nil + let kind: UInt32? = nevent.has_kind ? nevent.kind : nil + let relays = getRelayStrings(from: nevent.relays) + return .nevent(NEvent(noteid: note_id, relays: relays, author: pubkey, kind: kind)) + case NOSTR_BECH32_NPUB: + let npub = b.data.npub + let pubkey = Pubkey(Data(bytes: npub.pubkey, count: 32)) + return .npub(pubkey) + case NOSTR_BECH32_NSEC: + let nsec = b.data.nsec + let privkey = Privkey(Data(bytes: nsec.nsec, count: 32)) + guard let pubkey = privkey_to_pubkey(privkey: privkey) else { return nil } + return .npub(pubkey) + case NOSTR_BECH32_NPROFILE: + let nprofile = b.data.nprofile + let pubkey = Pubkey(Data(bytes: nprofile.pubkey, count: 32)) + return .nprofile(NProfile(author: pubkey, relays: getRelayStrings(from: nprofile.relays))) + case NOSTR_BECH32_NRELAY: + let nrelay = b.data.nrelay + let str_relay: str_block = nrelay.relay + guard let relay_str = String(str_relay) else { + return nil + } + return .nrelay(relay_str) + case NOSTR_BECH32_NADDR: + let naddr = b.data.naddr + guard let identifier = String(naddr.identifier) else { + return nil } + let pubkey = Pubkey(Data(bytes: naddr.pubkey, count: 32)) + let kind = naddr.kind + return .naddr(NAddr(identifier: identifier, author: pubkey, relays: getRelayStrings(from: naddr.relays), kind: kind)) + default: return nil } } + +private func getRelayStrings(from relays: relays) -> [String] { + var result = [String]() + let numRelays = Int(relays.num_relays) + + func processRelay(_ relay: str_block) { + if let string = String(relay) { + result.append(string) + } + } + + // Since relays is a C tuple, the indexes can't be iterated through so they need to be manually processed + if numRelays > 0 { processRelay(relays.relays.0) } + if numRelays > 1 { processRelay(relays.relays.1) } + if numRelays > 2 { processRelay(relays.relays.2) } + if numRelays > 3 { processRelay(relays.relays.3) } + if numRelays > 4 { processRelay(relays.relays.4) } + if numRelays > 5 { processRelay(relays.relays.5) } + if numRelays > 6 { processRelay(relays.relays.6) } + if numRelays > 7 { processRelay(relays.relays.7) } + if numRelays > 8 { processRelay(relays.relays.8) } + if numRelays > 9 { processRelay(relays.relays.9) } + + return result +} diff --git a/damusTests/Bech32ObjectTests.swift b/damusTests/Bech32ObjectTests.swift @@ -0,0 +1,118 @@ +// +// Bech32ObjectTests.swift +// damusTests +// +// Created by KernelKind on 1/5/24. +// +// This file contains tests that are adapted from the nostr-sdk-ios project. +// Original source: +// https://github.com/nostr-sdk/nostr-sdk-ios/blob/main/Tests/NostrSDKTests/MetadataCodingTests.swift +// + +import XCTest +@testable import damus + +class Bech32ObjectTests: XCTestCase { + func testTLVParsing_NeventHasRelaysNoAuthorNoKind_ValidContent() throws { + let content = "nevent1qqstna2yrezu5wghjvswqqculvvwxsrcvu7uc0f78gan4xqhvz49d9spr3mhxue69uhkummnw3ez6un9d3shjtn4de6x2argwghx6egpr4mhxue69uhkummnw3ez6ur4vgh8wetvd3hhyer9wghxuet5nxnepm" + let expectedNoteIDHex = "b9f5441e45ca39179320e0031cfb18e34078673dcc3d3e3a3b3a981760aa5696" + let relays = ["wss://nostr-relay.untethr.me", "wss://nostr-pub.wellorder.net"] + guard let noteid = hex_decode_noteid(expectedNoteIDHex) else { + XCTFail("Parsing note ID failed") + return + } + + let expectedObject = Bech32Object.nevent(NEvent(noteid: noteid, relays: relays)) + guard let actualObject = Bech32Object.parse(content) else { + XCTFail("Invalid Object") + return + } + + XCTAssertEqual(expectedObject, actualObject) + } + + func testTLVParsing_NeventHasRelaysNoAuthorHasKind_ValidContent() throws { + let content = "nevent1qqstna2yrezu5wghjvswqqculvvwxsrcvu7uc0f78gan4xqhvz49d9spr3mhxue69uhkummnw3ez6un9d3shjtn4de6x2argwghx6egpr4mhxue69uhkummnw3ez6ur4vgh8wetvd3hhyer9wghxuet5qvzqqqqqqyjyqz7d" + let expectedNoteIDHex = "b9f5441e45ca39179320e0031cfb18e34078673dcc3d3e3a3b3a981760aa5696" + let relays = ["wss://nostr-relay.untethr.me", "wss://nostr-pub.wellorder.net"] + guard let noteid = hex_decode_noteid(expectedNoteIDHex) else { + XCTFail("Parsing note ID failed") + return + } + + let expectedObject = Bech32Object.nevent(NEvent(noteid: noteid, relays: relays, kind: 1)) + guard let actualObject = Bech32Object.parse(content) else { + XCTFail("Invalid Object") + return + } + + XCTAssertEqual(expectedObject, actualObject) + } + + func testTLVParsing_NeventHasRelaysHasAuthorHasKind_ValidContent() throws { + let content = "nevent1qqstna2yrezu5wghjvswqqculvvwxsrcvu7uc0f78gan4xqhvz49d9spr3mhxue69uhkummnw3ez6un9d3shjtn4de6x2argwghx6egpr4mhxue69uhkummnw3ez6ur4vgh8wetvd3hhyer9wghxuet5qgsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8grqsqqqqqpw4032x" + + let expectedNoteIDHex = "b9f5441e45ca39179320e0031cfb18e34078673dcc3d3e3a3b3a981760aa5696" + let relays = ["wss://nostr-relay.untethr.me", "wss://nostr-pub.wellorder.net"] + guard let noteid = hex_decode_noteid(expectedNoteIDHex) else { + XCTFail("Parsing note ID failed") + return + } + guard let author = try bech32_decode("npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6") else { + XCTFail() + return + } + + let expectedObject = Bech32Object.nevent(NEvent(noteid: noteid, relays: relays, author: Pubkey(author.data), kind: 1)) + guard let actualObject = Bech32Object.parse(content) else { + XCTFail("Invalid Object") + return + } + + XCTAssertEqual(expectedObject, actualObject) + } + + func testTLVParsing_NProfileExample_ValidContent() throws { + let content = "nprofile1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gpp4mhxue69uhhytnc9e3k7mgpz4mhxue69uhkg6nzv9ejuumpv34kytnrdaksjlyr9p" + guard let author = try bech32_decode("npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6") else { + XCTFail() + return + } + let relays = ["wss://r.x.com", "wss://djbas.sadkb.com"] + + let expectedObject = Bech32Object.nprofile(NProfile(author: Pubkey(author.data), relays: relays)) + guard let actualObject = Bech32Object.parse(content) else { + XCTFail("Invalid Object") + return + } + + XCTAssertEqual(expectedObject, actualObject) + } + + func testTLVParsing_NRelayExample_ValidContent() throws { + let content = "nrelay1qqt8wumn8ghj7un9d3shjtnwdaehgu3wvfskueq4r295t" + let relay = "wss://relay.nostr.band" + + let expectedObject = Bech32Object.nrelay(relay) + let actualObject = Bech32Object.parse(content) + + XCTAssertEqual(expectedObject, actualObject) + } + + func testTLVParsing_NaddrExample_ValidContent() throws { + let content = "naddr1qqxnzdesxqmnxvpexqunzvpcqyt8wumn8ghj7un9d3shjtnwdaehgu3wvfskueqzypve7elhmamff3sr5mgxxms4a0rppkmhmn7504h96pfcdkpplvl2jqcyqqq823cnmhuld" + + guard let author = try bech32_decode("npub1tx0k0a7lw62vvqax6p3ku90tccgdka7ul4radews2wrdsg0m865szf9fw6") else { + XCTFail("Can't decode npub") + return + } + let relays = ["wss://relay.nostr.band"] + let identifier = "1700730909108" + let kind: UInt32 = 30023 + + let expectedObject = Bech32Object.naddr(NAddr(identifier: identifier, author: Pubkey(author.data), relays: relays, kind: kind)) + let actualObject = Bech32Object.parse(content) + + XCTAssertEqual(expectedObject, actualObject) + } +}