damus

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

Zap.swift (14261B)


      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 zap_req = get_zap_request(zap_ev) else {
    313             return nil
    314         }
    315 
    316         guard validate_event(ev: zap_req) == .ok else {
    317             return nil
    318         }
    319         
    320         guard let target = determine_zap_target(zap_req) else {
    321             return nil
    322         }
    323         
    324         let private_request = our_privkey.flatMap {
    325             decrypt_private_zap(our_privkey: $0, zapreq: zap_req, target: target)
    326         }
    327         
    328         let is_anon = private_request == nil && event_is_anonymous(ev: zap_req)
    329         let preq = private_request.map { pr in ZapRequest(ev: pr) }
    330         
    331         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)
    332     }
    333 }
    334 
    335 func decrypt_private_zap(our_privkey: Privkey, zapreq: NostrEvent, target: ZapTarget) -> NostrEvent? {
    336     guard let anon_tag = zapreq.tags.first(where: { t in
    337         t.count >= 2 && t[0].matches_str("anon")
    338     }) else {
    339         return nil
    340     }
    341     
    342     let enc_note = anon_tag[1].string()
    343 
    344     var note = decrypt_note(our_privkey: our_privkey, their_pubkey: zapreq.pubkey, enc_note: enc_note, encoding: .bech32)
    345     
    346     // check to see if the private note was from us
    347     if note == nil {
    348         guard let our_private_keypair = generate_private_keypair(our_privkey: our_privkey, id: NoteId(target.id), created_at: zapreq.created_at) else {
    349             return nil
    350         }
    351         // use our private keypair and their pubkey to get the shared secret
    352         note = decrypt_note(our_privkey: our_private_keypair.privkey, their_pubkey: target.pubkey, enc_note: enc_note, encoding: .bech32)
    353     }
    354     
    355     guard let note else {
    356         return nil
    357     }
    358         
    359     guard note.kind == 9733 else {
    360         return nil
    361     }
    362     
    363     let zr_etag = zapreq.referenced_ids.first
    364     let note_etag = note.referenced_ids.first
    365     
    366     guard zr_etag == note_etag else {
    367         return nil
    368     }
    369     
    370     let zr_ptag = zapreq.referenced_pubkeys.first
    371     let note_ptag = note.referenced_pubkeys.first
    372     
    373     guard let zr_ptag, let note_ptag, zr_ptag == note_ptag else {
    374         return nil
    375     }
    376     
    377     guard validate_event(ev: note) == .ok else {
    378         return nil
    379     }
    380     
    381     return note
    382 }
    383 
    384 func event_is_anonymous(ev: NostrEvent) -> Bool {
    385     return ev.known_kind == .zap_request && event_has_tag(ev: ev, tag: "anon")
    386 }
    387 
    388 func event_has_tag(ev: NostrEvent, tag: String) -> Bool {
    389     for t in ev.tags {
    390         if t.count >= 1 && t[0].matches_str(tag) {
    391             return true
    392         }
    393     }
    394     
    395     return false
    396 }
    397 
    398 func get_zap_request(_ ev: NostrEvent) -> NostrEvent? {
    399     guard let desc = event_tag(ev, name: "description") else {
    400         return nil
    401     }
    402 
    403     return decode_nostr_event_json(desc)
    404 }
    405 
    406 func invoice_to_zap_invoice(_ invoice: Invoice) -> ZapInvoice? {
    407     guard case .specific(let amt) = invoice.amount else {
    408         return nil
    409     }
    410     
    411     return ZapInvoice(description: invoice.description, amount: amt, string: invoice.string, expiry: invoice.expiry, payment_hash: invoice.payment_hash, created_at: invoice.created_at)
    412 }
    413 
    414 func determine_zap_target(_ ev: NostrEvent) -> ZapTarget? {
    415     guard let ptag = ev.referenced_pubkeys.first else {
    416         return nil
    417     }
    418     
    419     if let etag = ev.referenced_ids.first {
    420         return ZapTarget.note(id: etag, author: ptag)
    421     }
    422     
    423     return .profile(ptag)
    424 }
    425                    
    426 func decode_bolt11(_ s: String) -> Invoice? {
    427     var bs = note_blocks()
    428     bs.num_blocks = 0
    429     blocks_init(&bs)
    430     
    431     let bytes = s.utf8CString
    432     let _ = bytes.withUnsafeBufferPointer { p in
    433         damus_parse_content(&bs, p.baseAddress)
    434     }
    435     
    436     guard bs.num_blocks == 1 else {
    437         blocks_free(&bs)
    438         return nil
    439     }
    440     
    441     let block = bs.blocks[0]
    442     
    443     guard let converted = Block(block) else {
    444         blocks_free(&bs)
    445         return nil
    446     }
    447     
    448     guard case .invoice(let invoice) = converted else {
    449         blocks_free(&bs)
    450         return nil
    451     }
    452     
    453     blocks_free(&bs)
    454     return invoice
    455 }
    456 
    457 func event_tag(_ ev: NostrEvent, name: String) -> String? {
    458     for tag in ev.tags {
    459         if tag.count >= 2 && tag[0].matches_str(name) {
    460             return tag[1].string()
    461         }
    462     }
    463     
    464     return nil
    465 }
    466 
    467 func decode_nostr_event_json(_ desc: String) -> NostrEvent? {
    468     return NostrEvent.owned_from_json(json: desc)
    469 }
    470 
    471 
    472 func fetch_zapper_from_lnurl(lnurls: LNUrls, pubkey: Pubkey, lnurl: String) async -> Pubkey? {
    473     guard let endpoint = await lnurls.lookup_or_fetch(pubkey: pubkey, lnurl: lnurl),
    474           let allows = endpoint.allowsNostr, allows,
    475           let key = endpoint.nostrPubkey,
    476           let pk = hex_decode_pubkey(key)
    477     else {
    478         return nil
    479     }
    480     
    481     return pk
    482 }
    483 
    484 func fetch_zap_invoice(_ payreq: LNUrlPayRequest, zapreq: NostrEvent?, msats: Int64, zap_type: ZapType, comment: String?) async -> String? {
    485     guard var base_url = payreq.callback.flatMap({ URLComponents(string: $0) }) else {
    486         return nil
    487     }
    488     
    489     let zappable = payreq.allowsNostr ?? false
    490     
    491     var query = [URLQueryItem(name: "amount", value: "\(msats)")]
    492     
    493     if zappable && zap_type != .non_zap, let json = encode_json(zapreq) {
    494         print("zapreq json: \(json)")
    495         query.append(URLQueryItem(name: "nostr", value: json))
    496     }
    497    
    498     // add a lud12 comment as well if we have it
    499     if zap_type != .priv, let comment, let limit = payreq.commentAllowed, limit != 0 {
    500         let limited_comment = String(comment.prefix(limit))
    501         query.append(URLQueryItem(name: "comment", value: limited_comment))
    502     }
    503     
    504     base_url.queryItems = query
    505     
    506     guard let url = base_url.url else {
    507         return nil
    508     }
    509     
    510     print("url \(url)")
    511     
    512     var ret: (Data, URLResponse)? = nil
    513     do {
    514         ret = try await URLSession.shared.data(from: url)
    515     } catch {
    516         print(error.localizedDescription)
    517         return nil
    518     }
    519     
    520     guard let ret else {
    521         return nil
    522     }
    523     
    524     let json_str = String(decoding: ret.0, as: UTF8.self)
    525     guard let result: LNUrlPayResponse = decode_json(json_str) else {
    526         print("fetch_zap_invoice error: \(json_str)")
    527         return nil
    528     }
    529     
    530     // make sure it's the correct amount
    531     guard let bolt11 = decode_bolt11(result.pr),
    532           .specific(msats) == bolt11.amount
    533     else {
    534         return nil
    535     }
    536     
    537     return result.pr
    538 }