damus

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

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

nip19: add high level bech32 encoding method

Add encode method for converting Bech32Object to a bech32 encoded
string. This will be useful for generalized conversion of Bech32Objects
to its string representation.

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

Diffstat:
Mdamus/Util/Bech32Object.swift | 101+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
MdamusTests/Bech32ObjectTests.swift | 97+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 198 insertions(+), 0 deletions(-)

diff --git a/damus/Util/Bech32Object.swift b/damus/Util/Bech32Object.swift @@ -89,6 +89,24 @@ enum Bech32Object : Equatable { 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 decodeCBech32(_ b: nostr_bech32_t) -> Bech32Object? { @@ -162,3 +180,86 @@ private func getRelayStrings(from relays: relays) -> [String] { return result } + +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: [String]) { + for relay in relays where !relay.isEmpty { + guard let relayData = relay.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) +} diff --git a/damusTests/Bech32ObjectTests.swift b/damusTests/Bech32ObjectTests.swift @@ -115,4 +115,101 @@ class Bech32ObjectTests: XCTestCase { XCTAssertEqual(expectedObject, actualObject) } + + func testTLVEncoding_NeventHasRelaysNoAuthorNoKind_ValidContent() throws { + guard let noteid = hex_decode_noteid("b9f5441e45ca39179320e0031cfb18e34078673dcc3d3e3a3b3a981760aa5696") else { + XCTFail("Parsing note ID failed") + return + } + + let relays = ["wss://nostr-relay.untethr.me", "wss://nostr-pub.wellorder.net"] + + let expectedEncoding = "nevent1qqstna2yrezu5wghjvswqqculvvwxsrcvu7uc0f78gan4xqhvz49d9spr3mhxue69uhkummnw3ez6un9d3shjtn4de6x2argwghx6egpr4mhxue69uhkummnw3ez6ur4vgh8wetvd3hhyer9wghxuet5nxnepm" + + let actualEncoding = Bech32Object.encode(.nevent(NEvent(noteid: noteid, relays: relays))) + + XCTAssertEqual(expectedEncoding, actualEncoding) + } + + func testTLVEncoding_NeventHasRelaysNoAuthorHasKind_ValidContent() throws { + guard let noteid = hex_decode_noteid("b9f5441e45ca39179320e0031cfb18e34078673dcc3d3e3a3b3a981760aa5696") else { + XCTFail() + return + } + + let relays = [ + "wss://nostr-relay.untethr.me", + "wss://nostr-pub.wellorder.net" + ] + + let expectedEncoding = "nevent1qqstna2yrezu5wghjvswqqculvvwxsrcvu7uc0f78gan4xqhvz49d9spr3mhxue69uhkummnw3ez6un9d3shjtn4de6x2argwghx6egpr4mhxue69uhkummnw3ez6ur4vgh8wetvd3hhyer9wghxuet5qvzqqqqqqyjyqz7d" + + let actualEncoding = Bech32Object.encode(.nevent(NEvent(noteid: noteid, relays: relays, kind: 1))) + + XCTAssertEqual(expectedEncoding, actualEncoding) + } + + func testTLVEncoding_NeventHasRelaysHasAuthorHasKind_ValidContent() throws { + guard let noteid = hex_decode_noteid("b9f5441e45ca39179320e0031cfb18e34078673dcc3d3e3a3b3a981760aa5696") else { + XCTFail("Parsing note ID failed") + return + } + guard let author = try bech32_decode("npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6") else { + XCTFail() + return + } + + let relays = ["wss://nostr-relay.untethr.me", "wss://nostr-pub.wellorder.net"] + + let expectedEncoding = "nevent1qqstna2yrezu5wghjvswqqculvvwxsrcvu7uc0f78gan4xqhvz49d9spr3mhxue69uhkummnw3ez6un9d3shjtn4de6x2argwghx6egpr4mhxue69uhkummnw3ez6ur4vgh8wetvd3hhyer9wghxuet5qgsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8grqsqqqqqpw4032x" + + let actualEncoding = Bech32Object.encode(.nevent(NEvent(noteid: noteid, relays: relays, author: Pubkey(author.data), kind: 1))) + + XCTAssertEqual(expectedEncoding, actualEncoding) + } + + func testTLVEncoding_NProfileExample_ValidContent() throws { + guard let author = try bech32_decode("npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6") else { + XCTFail() + return + } + + let relays = [ + "wss://r.x.com", + "wss://djbas.sadkb.com" + ] + + let expectedEncoding = "nprofile1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gpp4mhxue69uhhytnc9e3k7mgpz4mhxue69uhkg6nzv9ejuumpv34kytnrdaksjlyr9p" + + let actualEncoding = Bech32Object.encode(.nprofile(NProfile(author: Pubkey(author.data), relays: relays))) + + XCTAssertEqual(expectedEncoding, actualEncoding) + } + + func testTLVEncoding_NRelayExample_ValidContent() throws { + let relay = "wss://relay.nostr.band" + + let expectedEncoding = "nrelay1qqt8wumn8ghj7un9d3shjtnwdaehgu3wvfskueq4r295t" + + let actualEncoding = Bech32Object.encode(.nrelay(relay)) + + XCTAssertEqual(expectedEncoding, actualEncoding) + } + + func testTLVEncoding_NaddrExample_ValidContent() throws { + guard let author = try bech32_decode("npub1tx0k0a7lw62vvqax6p3ku90tccgdka7ul4radews2wrdsg0m865szf9fw6") else { + XCTFail() + return + } + + let relays = ["wss://relay.nostr.band"] + let identifier = "1700730909108" + let kind: UInt32 = 30023 + + let expectedEncoding = "naddr1qqxnzdesxqmnxvpexqunzvpcqyt8wumn8ghj7un9d3shjtnwdaehgu3wvfskueqzypve7elhmamff3sr5mgxxms4a0rppkmhmn7504h96pfcdkpplvl2jqcyqqq823cnmhuld" + + let actualEncoding = Bech32Object.encode(.naddr(NAddr(identifier: identifier, author: Pubkey(author.data), relays: relays, kind: kind))) + + XCTAssertEqual(expectedEncoding, actualEncoding) + } }