damus

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

commit 967c4639763473240c59d520b04286ab5f981d3a
parent 510926fa707f67e65452aa9530214d5aec9fa1ff
Author: William Casarin <jb55@jb55.com>
Date:   Tue, 12 Apr 2022 06:47:54 -0700

Posting works!

Signed-off-by: William Casarin <jb55@jb55.com>

Diffstat:
Mdamus.xcodeproj/project.pbxproj | 39+++++++++++++++++++++++++++++++++++++++
Mdamus.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 9+++++++++
Mdamus/ContentView.swift | 7++++++-
Mdamus/Nostr/NostrEvent.swift | 140++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Adamus/Nostr/NostrRequest.swift | 18++++++++++++++++++
Mdamus/Nostr/RelayConnection.swift | 24++++++++++++++++++++----
Mdamus/Nostr/RelayPool.swift | 4++--
Mdamus/Views/EventView.swift | 2+-
8 files changed, 230 insertions(+), 13 deletions(-)

diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj @@ -27,6 +27,8 @@ 4CE6DF0427F7A08200C66700 /* damusUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE6DF0327F7A08200C66700 /* damusUITestsLaunchTests.swift */; }; 4CE6DF1227F7A2B300C66700 /* Starscream in Frameworks */ = {isa = PBXBuildFile; productRef = 4CE6DF1127F7A2B300C66700 /* Starscream */; }; 4CE6DF1627F8DEBF00C66700 /* RelayConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE6DF1527F8DEBF00C66700 /* RelayConnection.swift */; }; + 4CEE2AEB2805AEA300AB5EEF /* secp256k1 in Frameworks */ = {isa = PBXBuildFile; productRef = 4CEE2AEA2805AEA300AB5EEF /* secp256k1 */; }; + 4CEE2AED2805B22500AB5EEF /* NostrRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEE2AEC2805B22500AB5EEF /* NostrRequest.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -69,6 +71,8 @@ 4CE6DF0127F7A08200C66700 /* damusUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = damusUITests.swift; sourceTree = "<group>"; }; 4CE6DF0327F7A08200C66700 /* damusUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = damusUITestsLaunchTests.swift; sourceTree = "<group>"; }; 4CE6DF1527F8DEBF00C66700 /* RelayConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayConnection.swift; sourceTree = "<group>"; }; + 4CEE2AE72804F57C00AB5EEF /* libsecp256k1.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libsecp256k1.a; sourceTree = "<group>"; }; + 4CEE2AEC2805B22500AB5EEF /* NostrRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrRequest.swift; sourceTree = "<group>"; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -76,6 +80,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 4CEE2AEB2805AEA300AB5EEF /* secp256k1 in Frameworks */, 4CE6DF1227F7A2B300C66700 /* Starscream in Frameworks */, 4C75EFAA28049C9F0006080F /* CachedAsyncImage in Frameworks */, ); @@ -119,6 +124,7 @@ 4C75EFB428049D790006080F /* Relay.swift */, 4C75EFB628049D990006080F /* RelayPool.swift */, 4C75EFBA2804A34C0006080F /* ProofOfWork.swift */, + 4CEE2AEC2805B22500AB5EEF /* NostrRequest.swift */, ); path = Nostr; sourceTree = "<group>"; @@ -130,6 +136,7 @@ 4CE6DEF627F7A08200C66700 /* damusTests */, 4CE6DF0027F7A08200C66700 /* damusUITests */, 4CE6DEE427F7A08100C66700 /* Products */, + 4CEE2AE62804F57B00AB5EEF /* Frameworks */, ); sourceTree = "<group>"; }; @@ -182,6 +189,14 @@ path = damusUITests; sourceTree = "<group>"; }; + 4CEE2AE62804F57B00AB5EEF /* Frameworks */ = { + isa = PBXGroup; + children = ( + 4CEE2AE72804F57C00AB5EEF /* libsecp256k1.a */, + ); + name = Frameworks; + sourceTree = "<group>"; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -201,6 +216,7 @@ packageProductDependencies = ( 4CE6DF1127F7A2B300C66700 /* Starscream */, 4C75EFA928049C9F0006080F /* CachedAsyncImage */, + 4CEE2AEA2805AEA300AB5EEF /* secp256k1 */, ); productName = damus; productReference = 4CE6DEE327F7A08100C66700 /* damus.app */; @@ -277,6 +293,7 @@ packageReferences = ( 4CE6DF1027F7A2B300C66700 /* XCRemoteSwiftPackageReference "Starscream" */, 4C75EFA828049C9F0006080F /* XCRemoteSwiftPackageReference "swiftui-cached-async-image" */, + 4CEE2AE92805AEA300AB5EEF /* XCRemoteSwiftPackageReference "secp256k1" */, ); productRefGroup = 4CE6DEE427F7A08100C66700 /* Products */; projectDirPath = ""; @@ -330,6 +347,7 @@ 4C75EFAF28049D350006080F /* NostrFilter.swift in Sources */, 4CE6DF1627F8DEBF00C66700 /* RelayConnection.swift in Sources */, 4CE6DEE727F7A08100C66700 /* damusApp.swift in Sources */, + 4CEE2AED2805B22500AB5EEF /* NostrRequest.swift in Sources */, 4C75EFA427FA577B0006080F /* PostView.swift in Sources */, 4C75EFB528049D790006080F /* Relay.swift in Sources */, 4C75EFBB2804A34C0006080F /* ProofOfWork.swift in Sources */, @@ -506,6 +524,10 @@ "$(inherited)", "@executable_path/Frameworks", ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)", + ); MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -536,6 +558,10 @@ "$(inherited)", "@executable_path/Frameworks", ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)", + ); MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -679,6 +705,14 @@ minimumVersion = 4.0.0; }; }; + 4CEE2AE92805AEA300AB5EEF /* XCRemoteSwiftPackageReference "secp256k1" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/GigaBitcoin/secp256k1.swift"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 0.5.0; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -692,6 +726,11 @@ package = 4CE6DF1027F7A2B300C66700 /* XCRemoteSwiftPackageReference "Starscream" */; productName = Starscream; }; + 4CEE2AEA2805AEA300AB5EEF /* secp256k1 */ = { + isa = XCSwiftPackageProductDependency; + package = 4CEE2AE92805AEA300AB5EEF /* XCRemoteSwiftPackageReference "secp256k1" */; + productName = secp256k1; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 4CE6DEDB27F7A08100C66700 /* Project object */; diff --git a/damus.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/damus.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,6 +1,15 @@ { "pins" : [ { + "identity" : "secp256k1.swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/GigaBitcoin/secp256k1.swift", + "state" : { + "revision" : "abe7c8232970c1fd57f4c77590bce2c868df7137", + "version" : "0.5.0" + } + }, + { "identity" : "starscream", "kind" : "remoteSourceControl", "location" : "https://github.com/daltoniam/Starscream", diff --git a/damus/ContentView.swift b/damus/ContentView.swift @@ -74,6 +74,11 @@ struct ContentView: View { .onReceive(NotificationCenter.default.publisher(for: .post)) { obj in let post = obj.object as! NostrPost print("post \(post.content)") + let pubkey = "" + let privkey = "" + let new_ev = NostrEvent(content: post.content, pubkey: pubkey) + new_ev.sign(privkey: privkey) + self.pool?.send(.event(new_ev)) } } @@ -129,7 +134,7 @@ struct ContentView: View { self.sub_id = sub_id } print("subscribing to \(sub_id)") - self.pool?.send(filters: filters, sub_id: sub_id) + self.pool?.send(.subscribe(.init(filters: filters, sub_id: sub_id))) } func handle_event(relay_id: String, conn_event: NostrConnectionEvent) { diff --git a/damus/Nostr/NostrEvent.swift b/damus/Nostr/NostrEvent.swift @@ -6,6 +6,8 @@ // import Foundation +import CommonCrypto +import secp256k1 struct OtherEvent { let event_id: String @@ -17,15 +19,84 @@ struct KeyEvent { let relay_url: String } -struct NostrEvent: Decodable, Identifiable { - let id: String +class NostrEvent: Codable, Identifiable { + var id: String + var sig: String + var tags: [[String]] + + // cached field for pow calc + var pow: Int? + let pubkey: String let created_at: Int64 let kind: Int - let tags: [[String]] let content: String - let sig: String - var pow: Int? + + private enum CodingKeys: String, CodingKey { + case id, sig, tags, pubkey, created_at, kind, content + } + + init(content: String, pubkey: String, kind: Int = 1, tags: [[String]] = []) { + self.id = "" + self.sig = "" + + self.content = content + self.pubkey = pubkey + self.kind = kind + self.tags = tags + self.created_at = Int64(Date().timeIntervalSince1970) + + self.calculate_id() + } + + func calculate_id() { + self.id = calculate_event_id(ev: self) + self.pow = count_hash_leading_zero_bits(self.id) + } + + // TODO: timeout + /* + func mine_id(pow: Int, done: @escaping (String) -> ()) { + let nonce_ind = self.ensure_nonce_tag() + let nonce: Int64 = 0 + + DispatchQueue.global(qos: .background).async { + while + } + } + */ + + private func ensure_nonce_tag() -> Int { + for (i, tags) in self.tags.enumerated() { + for tag in tags { + if tags.count == 2 && tag == "nonce" { + return i + } + } + } + + self.tags.append(["nonce", "0"]) + return self.tags.count - 1 + } + + func sign(privkey: String) { + self.sig = sign_event(privkey: privkey, ev: self) + } +} + +func sign_event(privkey: String, ev: NostrEvent) -> String { + let priv_key_bytes = try! privkey.byteArray() + let key = try! secp256k1.Signing.PrivateKey(rawRepresentation: priv_key_bytes) + + // Extra params for custom signing + + var aux_rand = random_bytes(count: 64) + var digest = try! ev.id.byteArray() + + // API allows for signing variable length messages + let signature = try! key.schnorr.signature(message: &digest, auxiliaryRand: &aux_rand) + + return hex_encode(signature.rawRepresentation) } func decode_nostr_event(txt: String) -> NostrResponse? { @@ -42,3 +113,62 @@ func decode_data<T: Decodable>(_ data: Data) -> T? { return nil } + +func event_commitment(ev: NostrEvent, tags: String) -> String { + return "[0,\"\(ev.pubkey)\",\(ev.created_at),\(ev.kind),\(tags),\"\(ev.content)\"]" +} + +func calculate_event_id(ev: NostrEvent) -> String { + let tags_encoder = JSONEncoder() + let tags_data = try! tags_encoder.encode(ev.tags) + let tags = String(decoding: tags_data, as: UTF8.self) + + let target = event_commitment(ev: ev, tags: tags) + let target_data = target.data(using: .utf8)! + let hash = sha256(target_data) + + return hex_encode(hash) +} + + +func sha256(_ data: Data) -> Data { + var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH)) + data.withUnsafeBytes { + _ = CC_SHA256($0.baseAddress, CC_LONG(data.count), &hash) + } + return Data(hash) +} + +func hexchar(_ val: UInt8) -> UInt8 { + if val < 10 { + return 48 + val; + } + if val < 16 { + return 97 + val - 10; + } + assertionFailure("impossiburu") + return 0 +} + + +func hex_encode(_ data: Data) -> String { + var str = "" + for c in data { + let c1 = hexchar(c >> 4) + let c2 = hexchar(c & 0xF) + + str.append(Character(Unicode.Scalar(c1))) + str.append(Character(Unicode.Scalar(c2))) + } + return str +} + + + +func random_bytes(count: Int) -> Data { + var data = Data(count: count) + _ = data.withUnsafeMutableBytes { mutableBytes in + SecRandomCopyBytes(kSecRandomDefault, count, mutableBytes) + } + return data +} diff --git a/damus/Nostr/NostrRequest.swift b/damus/Nostr/NostrRequest.swift @@ -0,0 +1,18 @@ +// +// NostrRequest.swift +// damus +// +// Created by William Casarin on 2022-04-12. +// + +import Foundation + +struct NostrSubscribe { + let filters: [NostrFilter] + let sub_id: String +} + +enum NostrRequest { + case subscribe(NostrSubscribe) + case event(NostrEvent) +} diff --git a/damus/Nostr/RelayConnection.swift b/damus/Nostr/RelayConnection.swift @@ -35,9 +35,9 @@ class RelayConnection: WebSocketDelegate { socket.disconnect() } - func send(_ filters: [NostrFilter], sub_id: String) { - guard let req = make_nostr_req(filters, sub_id: sub_id) else { - print("failed to encode nostr req: \(filters)") + func send(_ req: NostrRequest) { + guard let req = make_nostr_req(req) else { + print("failed to encode nostr req: \(req)") return } socket.write(string: req) @@ -71,7 +71,23 @@ class RelayConnection: WebSocketDelegate { } -func make_nostr_req(_ filters: [NostrFilter], sub_id: String) -> String? { +func make_nostr_req(_ req: NostrRequest) -> String? { + switch req { + case .subscribe(let sub): + return make_nostr_subscription_req(sub.filters, sub_id: sub.sub_id) + case .event(let ev): + return make_nostr_push_event(ev: ev) + } +} + +func make_nostr_push_event(ev: NostrEvent) -> String? { + let encoder = JSONEncoder() + let event_data = try! encoder.encode(ev) + let event = String(decoding: event_data, as: UTF8.self) + return "[\"EVENT\",\(event)]" +} + +func make_nostr_subscription_req(_ filters: [NostrFilter], sub_id: String) -> String? { let encoder = JSONEncoder() var req = "[\"REQ\",\"\(sub_id)\"" for filter in filters { diff --git a/damus/Nostr/RelayPool.swift b/damus/Nostr/RelayPool.swift @@ -34,12 +34,12 @@ class RelayPool { } } - func send(filters: [NostrFilter], sub_id: String, to: [String]? = nil) { + func send(_ req: NostrRequest, to: [String]? = nil) { let relays = to.map{ get_relays($0) } ?? self.relays for relay in relays { if relay.connection.isConnected { - relay.connection.send(filters, sub_id: sub_id) + relay.connection.send(req) } } } diff --git a/damus/Views/EventView.swift b/damus/Views/EventView.swift @@ -63,5 +63,5 @@ struct EventView: View { func calculate_pow_color(_ pow: Int) -> Color { let x = Double(pow) / 30.0; - return Color(.sRGB, red: 2.0 * (1.0 - x), green: 2.0 * x, blue: 0, opacity: 1.0) + return Color(.sRGB, red: 2.0 * (1.0 - x), green: 2.0 * x, blue: 0, opacity: 0.5) }