damus

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

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 }