Zap.swift (14823B)
1 // 2 // Zap.swift 3 // damus 4 // 5 // Created by William Casarin on 2023-01-15. 6 // 7 8 import Foundation 9 10 struct NoteZapTarget: Equatable, Hashable { 11 public let note_id: NoteId 12 public let author: Pubkey 13 } 14 15 enum ZapTarget: Equatable, Hashable { 16 case profile(Pubkey) 17 case note(NoteZapTarget) 18 19 static func note(id: NoteId, author: Pubkey) -> ZapTarget { 20 return .note(NoteZapTarget(note_id: id, author: author)) 21 } 22 23 var pubkey: Pubkey { 24 switch self { 25 case .profile(let pk): 26 return pk 27 case .note(let note_target): 28 return note_target.author 29 } 30 } 31 32 var note_id: NoteId? { 33 switch self { 34 case .profile: 35 return nil 36 case .note(let noteZapTarget): 37 return noteZapTarget.note_id 38 } 39 } 40 41 var id: Data { 42 switch self { 43 case .profile(let pubkey): 44 return pubkey.id 45 case .note(let noteZapTarget): 46 return noteZapTarget.note_id.id 47 } 48 } 49 } 50 51 struct ZapRequest { 52 let ev: NostrEvent 53 let marked_hidden: Bool 54 55 var id: ZapRequestId { 56 ZapRequestId(from_zap_request: self) 57 } 58 59 var is_in_thread: Bool { 60 return !self.ev.content.isEmpty && !marked_hidden 61 } 62 63 init(ev: NostrEvent) { 64 self.ev = ev 65 self.marked_hidden = ev.tags.first(where: { t in t.count > 0 && t[0].matches_str("hidden") }) != nil 66 } 67 } 68 69 enum ExtPendingZapStateType { 70 case fetching_invoice 71 case done 72 } 73 74 class ExtPendingZapState: Equatable { 75 static func == (lhs: ExtPendingZapState, rhs: ExtPendingZapState) -> Bool { 76 return lhs.state == rhs.state 77 } 78 79 var state: ExtPendingZapStateType 80 81 init(state: ExtPendingZapStateType) { 82 self.state = state 83 } 84 } 85 86 enum PendingZapState: Equatable { 87 case nwc(NWCPendingZapState) 88 case external(ExtPendingZapState) 89 } 90 91 92 enum NWCStateType: Equatable { 93 case fetching_invoice 94 case cancel_fetching_invoice 95 case postbox_pending(NostrEvent) 96 case confirmed 97 case failed 98 } 99 100 class NWCPendingZapState: Equatable { 101 private(set) var state: NWCStateType 102 let url: WalletConnectURL 103 104 init(state: NWCStateType, url: WalletConnectURL) { 105 self.state = state 106 self.url = url 107 } 108 109 //@discardableResult -- not discardable, the ZapsDataModel may need to send objectWillChange but we don't force it 110 func update_state(state: NWCStateType) -> Bool { 111 guard state != self.state else { 112 return false 113 } 114 self.state = state 115 return true 116 } 117 118 static func == (lhs: NWCPendingZapState, rhs: NWCPendingZapState) -> Bool { 119 return lhs.state == rhs.state && lhs.url == rhs.url 120 } 121 } 122 123 class PendingZap { 124 let amount_msat: Int64 125 let target: ZapTarget 126 let request: ZapRequest 127 let type: ZapType 128 private(set) var state: PendingZapState 129 130 init(amount_msat: Int64, target: ZapTarget, request: MakeZapRequest, type: ZapType, state: PendingZapState) { 131 self.amount_msat = amount_msat 132 self.target = target 133 self.request = request.private_inner_request 134 self.type = type 135 self.state = state 136 } 137 138 @discardableResult 139 func update_state(model: ZapsDataModel, state: PendingZapState) -> Bool { 140 guard self.state != state else { 141 return false 142 } 143 144 self.state = state 145 model.objectWillChange.send() 146 return true 147 } 148 } 149 150 struct ZapRequestId: Equatable, Hashable { 151 let reqid: NoteId 152 153 init(from_zap_request: ZapRequest) { 154 self.reqid = from_zap_request.ev.id 155 } 156 157 init(from_zap: Zapping) { 158 self.reqid = from_zap.request.ev.id 159 } 160 161 init(from_makezap: MakeZapRequest) { 162 self.reqid = from_makezap.private_inner_request.ev.id 163 } 164 165 init(from_pending: PendingZap) { 166 self.reqid = from_pending.request.ev.id 167 } 168 } 169 170 enum Zapping { 171 case zap(Zap) 172 case pending(PendingZap) 173 174 var is_pending: Bool { 175 switch self { 176 case .zap: 177 return false 178 case .pending: 179 return true 180 } 181 } 182 183 var is_paid: Bool { 184 switch self { 185 case .zap: 186 // we have a zap so this is proof of payment 187 return true 188 case .pending(let pzap): 189 switch pzap.state { 190 case .external: 191 // It could be but we don't know. We have to wait for a zap to know. 192 return false 193 case .nwc(let nwc_state): 194 // nwc confirmed that we have a payment, but we might not have zap yet 195 return nwc_state.state == .confirmed 196 } 197 } 198 } 199 200 var is_private: Bool { 201 switch self { 202 case .zap(let zap): 203 return zap.private_request != nil 204 case .pending(let pzap): 205 return pzap.type == .priv 206 } 207 } 208 209 var amount: Int64 { 210 switch self { 211 case .zap(let zap): 212 return zap.invoice.amount 213 case .pending(let pzap): 214 return pzap.amount_msat 215 } 216 } 217 218 var target: ZapTarget { 219 switch self { 220 case .zap(let zap): 221 return zap.target 222 case .pending(let pzap): 223 return pzap.target 224 } 225 } 226 227 var request: ZapRequest { 228 switch self { 229 case .zap(let zap): 230 return zap.request 231 case .pending(let pzap): 232 return pzap.request 233 } 234 } 235 236 var created_at: UInt32 { 237 switch self { 238 case .zap(let zap): 239 return zap.event.created_at 240 case .pending(let pzap): 241 // pending zaps are created right away 242 return pzap.request.ev.created_at 243 } 244 } 245 246 var event: NostrEvent? { 247 switch self { 248 case .zap(let zap): 249 return zap.event 250 case .pending: 251 // pending zaps don't have a zap event 252 return nil 253 } 254 } 255 256 var is_in_thread: Bool { 257 switch self { 258 case .zap(let zap): 259 return zap.request.is_in_thread 260 case .pending(let pzap): 261 return pzap.request.is_in_thread 262 } 263 } 264 265 var is_anon: Bool { 266 switch self { 267 case .zap(let zap): 268 return zap.is_anon 269 case .pending(let pzap): 270 return pzap.type == .anon 271 } 272 } 273 } 274 275 struct Zap { 276 public let event: NostrEvent 277 public let invoice: ZapInvoice 278 public let zapper: Pubkey /// zap authorizer 279 public let target: ZapTarget 280 public let raw_request: ZapRequest 281 public let is_anon: Bool 282 public let private_request: ZapRequest? 283 284 var request: ZapRequest { 285 return private_request ?? self.raw_request 286 } 287 288 public static func from_zap_event(zap_ev: NostrEvent, zapper: Pubkey, our_privkey: Privkey?) -> Zap? { 289 /// Make sure that we only create a zap event if it is authorized by the profile or event 290 guard zapper == zap_ev.pubkey else { 291 return nil 292 } 293 guard let bolt11_str = event_tag(zap_ev, name: "bolt11") else { 294 return nil 295 } 296 guard let bolt11 = decode_bolt11(bolt11_str) else { 297 return nil 298 } 299 /// Any amount invoices are not allowed 300 guard let zap_invoice = invoice_to_zap_invoice(bolt11) else { 301 return nil 302 } 303 // Some endpoints don't have this, let's skip the check for now. We're mostly trusting the zapper anyways 304 /* 305 guard let preimage = event_tag(zap_ev, name: "preimage") else { 306 return nil 307 } 308 guard preimage_matches_invoice(preimage, inv: zap_invoice) else { 309 return nil 310 } 311 */ 312 guard let desc = get_zap_description(zap_ev, inv_desc: zap_invoice.description) else { 313 return nil 314 } 315 316 guard let zap_req = decode_nostr_event_json(desc) else { 317 return nil 318 } 319 320 guard validate_event(ev: zap_req) == .ok else { 321 return nil 322 } 323 324 guard let target = determine_zap_target(zap_req) else { 325 return nil 326 } 327 328 let private_request = our_privkey.flatMap { 329 decrypt_private_zap(our_privkey: $0, zapreq: zap_req, target: target) 330 } 331 332 let is_anon = private_request == nil && event_is_anonymous(ev: zap_req) 333 let preq = private_request.map { pr in ZapRequest(ev: pr) } 334 335 return Zap(event: zap_ev, invoice: zap_invoice, zapper: zapper, target: target, raw_request: ZapRequest(ev: zap_req), is_anon: is_anon, private_request: preq) 336 } 337 } 338 339 func decrypt_private_zap(our_privkey: Privkey, zapreq: NostrEvent, target: ZapTarget) -> NostrEvent? { 340 guard let anon_tag = zapreq.tags.first(where: { t in 341 t.count >= 2 && t[0].matches_str("anon") 342 }) else { 343 return nil 344 } 345 346 let enc_note = anon_tag[1].string() 347 348 var note = decrypt_note(our_privkey: our_privkey, their_pubkey: zapreq.pubkey, enc_note: enc_note, encoding: .bech32) 349 350 // check to see if the private note was from us 351 if note == nil { 352 guard let our_private_keypair = generate_private_keypair(our_privkey: our_privkey, id: NoteId(target.id), created_at: zapreq.created_at) else { 353 return nil 354 } 355 // use our private keypair and their pubkey to get the shared secret 356 note = decrypt_note(our_privkey: our_private_keypair.privkey, their_pubkey: target.pubkey, enc_note: enc_note, encoding: .bech32) 357 } 358 359 guard let note else { 360 return nil 361 } 362 363 guard note.kind == 9733 else { 364 return nil 365 } 366 367 let zr_etag = zapreq.referenced_ids.first 368 let note_etag = note.referenced_ids.first 369 370 guard zr_etag == note_etag else { 371 return nil 372 } 373 374 let zr_ptag = zapreq.referenced_pubkeys.first 375 let note_ptag = note.referenced_pubkeys.first 376 377 guard let zr_ptag, let note_ptag, zr_ptag == note_ptag else { 378 return nil 379 } 380 381 guard validate_event(ev: note) == .ok else { 382 return nil 383 } 384 385 return note 386 } 387 388 func event_is_anonymous(ev: NostrEvent) -> Bool { 389 return ev.known_kind == .zap_request && event_has_tag(ev: ev, tag: "anon") 390 } 391 392 func event_has_tag(ev: NostrEvent, tag: String) -> Bool { 393 for t in ev.tags { 394 if t.count >= 1 && t[0].matches_str(tag) { 395 return true 396 } 397 } 398 399 return false 400 } 401 402 /// Fetches the description from either the invoice, or tags, depending on the type of invoice 403 func get_zap_description(_ ev: NostrEvent, inv_desc: InvoiceDescription) -> String? { 404 switch inv_desc { 405 case .description(let string): 406 return string 407 case .description_hash(let deschash): 408 guard let desc = event_tag(ev, name: "description") else { 409 return nil 410 } 411 guard let data = desc.data(using: .utf8) else { 412 return nil 413 } 414 guard sha256(data) == deschash else { 415 return nil 416 } 417 418 return desc 419 } 420 } 421 422 func invoice_to_zap_invoice(_ invoice: Invoice) -> ZapInvoice? { 423 guard case .specific(let amt) = invoice.amount else { 424 return nil 425 } 426 427 return ZapInvoice(description: invoice.description, amount: amt, string: invoice.string, expiry: invoice.expiry, payment_hash: invoice.payment_hash, created_at: invoice.created_at) 428 } 429 430 func determine_zap_target(_ ev: NostrEvent) -> ZapTarget? { 431 guard let ptag = ev.referenced_pubkeys.first else { 432 return nil 433 } 434 435 if let etag = ev.referenced_ids.first { 436 return ZapTarget.note(id: etag, author: ptag) 437 } 438 439 return .profile(ptag) 440 } 441 442 func decode_bolt11(_ s: String) -> Invoice? { 443 var bs = note_blocks() 444 bs.num_blocks = 0 445 blocks_init(&bs) 446 447 let bytes = s.utf8CString 448 let _ = bytes.withUnsafeBufferPointer { p in 449 damus_parse_content(&bs, p.baseAddress) 450 } 451 452 guard bs.num_blocks == 1 else { 453 blocks_free(&bs) 454 return nil 455 } 456 457 let block = bs.blocks[0] 458 459 guard let converted = Block(block) else { 460 blocks_free(&bs) 461 return nil 462 } 463 464 guard case .invoice(let invoice) = converted else { 465 blocks_free(&bs) 466 return nil 467 } 468 469 blocks_free(&bs) 470 return invoice 471 } 472 473 func event_tag(_ ev: NostrEvent, name: String) -> String? { 474 for tag in ev.tags { 475 if tag.count >= 2 && tag[0].matches_str(name) { 476 return tag[1].string() 477 } 478 } 479 480 return nil 481 } 482 483 func decode_nostr_event_json(_ desc: String) -> NostrEvent? { 484 return NostrEvent.owned_from_json(json: desc) 485 } 486 487 488 func fetch_zapper_from_lnurl(lnurls: LNUrls, pubkey: Pubkey, lnurl: String) async -> Pubkey? { 489 guard let endpoint = await lnurls.lookup_or_fetch(pubkey: pubkey, lnurl: lnurl), 490 let allows = endpoint.allowsNostr, allows, 491 let key = endpoint.nostrPubkey, 492 let pk = hex_decode_pubkey(key) 493 else { 494 return nil 495 } 496 497 return pk 498 } 499 500 func fetch_zap_invoice(_ payreq: LNUrlPayRequest, zapreq: NostrEvent?, msats: Int64, zap_type: ZapType, comment: String?) async -> String? { 501 guard var base_url = payreq.callback.flatMap({ URLComponents(string: $0) }) else { 502 return nil 503 } 504 505 let zappable = payreq.allowsNostr ?? false 506 507 var query = [URLQueryItem(name: "amount", value: "\(msats)")] 508 509 if zappable && zap_type != .non_zap, let json = encode_json(zapreq) { 510 print("zapreq json: \(json)") 511 query.append(URLQueryItem(name: "nostr", value: json)) 512 } 513 514 // add a lud12 comment as well if we have it 515 if zap_type != .priv, let comment, let limit = payreq.commentAllowed, limit != 0 { 516 let limited_comment = String(comment.prefix(limit)) 517 query.append(URLQueryItem(name: "comment", value: limited_comment)) 518 } 519 520 base_url.queryItems = query 521 522 guard let url = base_url.url else { 523 return nil 524 } 525 526 print("url \(url)") 527 528 var ret: (Data, URLResponse)? = nil 529 do { 530 ret = try await URLSession.shared.data(from: url) 531 } catch { 532 print(error.localizedDescription) 533 return nil 534 } 535 536 guard let ret else { 537 return nil 538 } 539 540 let json_str = String(decoding: ret.0, as: UTF8.self) 541 guard let result: LNUrlPayResponse = decode_json(json_str) else { 542 print("fetch_zap_invoice error: \(json_str)") 543 return nil 544 } 545 546 // make sure it's the correct amount 547 guard let bolt11 = decode_bolt11(result.pr), 548 .specific(msats) == bolt11.amount 549 else { 550 return nil 551 } 552 553 return result.pr 554 }