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:
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)
+ }
+}