damus

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

WalletConnect+.swift (9655B)


      1 //
      2 //  WalletConnect+.swift
      3 //  damus
      4 //
      5 //  Created by Daniel D’Aquino on 2023-11-27.
      6 //
      7 
      8 import Foundation
      9 
     10 // TODO: Eventually we should move these convenience functions into structured classes responsible for managing this type of functionality, such as `WalletModel`
     11 
     12 extension WalletConnect {
     13     /// Creates and sends a subscription to an NWC relay requesting NWC responses to be sent back.
     14     ///
     15     /// Notes: This assumes there is already a listener somewhere else
     16     ///
     17     /// - Parameters:
     18     ///   - url: The Nostr Wallet Connect URL containing connection info to the NWC wallet
     19     ///   - pool: The RelayPool to send the subscription request through
     20     static func subscribe(url: WalletConnectURL, pool: RelayPool) {
     21         var filter = NostrFilter(kinds: [.nwc_response])
     22         filter.authors = [url.pubkey]
     23         filter.pubkeys = [url.keypair.pubkey]
     24         filter.limit = 0
     25         let sub = NostrSubscribe(filters: [filter], sub_id: "nwc")
     26 
     27         pool.send(.subscribe(sub), to: [url.relay], skip_ephemeral: false)
     28     }
     29 
     30     /// Sends out a request to pay an invoice to the NWC relay, and ensures that:
     31     /// 1. the NWC relay is connected and we are listening to NWC events
     32     /// 2. the NWC relay is connected and we are listening to NWC
     33     ///
     34     /// Note: This does not return information about whether the payment is succesful or not. The actual confirmation is handled elsewhere around `HomeModel` and `WalletModel`
     35     ///
     36     /// - Parameters:
     37     ///   - url: The NWC wallet connection URL
     38     ///   - pool: The relay pool to connect to
     39     ///   - post: The postbox to send events in
     40     ///   - delay: The delay before actually sending the request to the network _(this makes it possible to cancel a zap)_
     41     ///   - on_flush: A callback to call after the event has been flushed to the network
     42     /// - Returns: The Nostr Event that was sent to the network, representing the request that was made
     43     @discardableResult
     44     static func pay(url: WalletConnectURL, pool: RelayPool, post: PostBox, invoice: String, zap_request: NostrEvent?, delay: TimeInterval? = 5.0, on_flush: OnFlush? = nil) -> NostrEvent? {
     45         
     46         let req = WalletConnect.Request.payZapRequest(invoice: invoice, zapRequest: zap_request)
     47         guard let ev = req.to_nostr_event(to_pk: url.pubkey, keypair: url.keypair) else {
     48             return nil
     49         }
     50 
     51         try? pool.add_relay(.nwc(url: url.relay))   // Ensure the NWC relay is connected
     52         WalletConnect.subscribe(url: url, pool: pool)      // Ensure we are listening to NWC updates from the relay
     53         post.send(ev, to: [url.relay], skip_ephemeral: false, delay: delay, on_flush: on_flush)
     54         return ev
     55     }
     56 
     57     /// Sends out a wallet balance request to the NWC relay, and ensures that:
     58     /// 1. the NWC relay is connected and we are listening to NWC events
     59     /// 2. the NWC relay is connected and we are listening to NWC
     60     ///
     61     /// Note: This does not return the actual balance information. The actual balance is handled elsewhere around `HomeModel` and `WalletModel`
     62     ///
     63     /// - Parameters:
     64     ///   - url: The NWC wallet connection URL
     65     ///   - pool: The relay pool to connect to
     66     ///   - post: The postbox to send events in
     67     ///   - delay: The delay before actually sending the request to the network
     68     ///   - on_flush: A callback to call after the event has been flushed to the network
     69     /// - Returns: The Nostr Event that was sent to the network, representing the request that was made
     70     @discardableResult
     71     static func request_balance_information(url: WalletConnectURL, pool: RelayPool, post: PostBox, delay: TimeInterval? = 0.0, on_flush: OnFlush? = nil) -> NostrEvent? {
     72         let req = WalletConnect.Request.getBalance
     73         guard let ev = req.to_nostr_event(to_pk: url.pubkey, keypair: url.keypair) else {
     74             return nil
     75         }
     76 
     77         try? pool.add_relay(.nwc(url: url.relay))   // Ensure the NWC relay is connected
     78         WalletConnect.subscribe(url: url, pool: pool)      // Ensure we are listening to NWC updates from the relay
     79         post.send(ev, to: [url.relay], skip_ephemeral: false, delay: delay, on_flush: on_flush)
     80         return ev
     81     }
     82 
     83     /// Sends out a wallet transaction list request to the NWC relay, and ensures that:
     84     /// 1. the NWC relay is connected and we are listening to NWC events
     85     /// 2. the NWC relay is connected and we are listening to NWC
     86     ///
     87     /// Note: This does not return the actual transaction list. The actual transaction list is handled elsewhere around `HomeModel` and `WalletModel`
     88     ///
     89     /// - Parameters:
     90     ///   - url: The NWC wallet connection URL
     91     ///   - pool: The relay pool to connect to
     92     ///   - post: The postbox to send events in
     93     ///   - delay: The delay before actually sending the request to the network
     94     ///   - on_flush: A callback to call after the event has been flushed to the network
     95     /// - Returns: The Nostr Event that was sent to the network, representing the request that was made
     96     @discardableResult
     97     static func request_transaction_list(url: WalletConnectURL, pool: RelayPool, post: PostBox, delay: TimeInterval? = 0.0, on_flush: OnFlush? = nil) -> NostrEvent? {
     98         let req = WalletConnect.Request.getTransactionList(from: nil, until: nil, limit: 10, offset: 0, unpaid: false, type: "")
     99         guard let ev = req.to_nostr_event(to_pk: url.pubkey, keypair: url.keypair) else {
    100             return nil
    101         }
    102 
    103         try? pool.add_relay(.nwc(url: url.relay))   // Ensure the NWC relay is connected
    104         WalletConnect.subscribe(url: url, pool: pool)      // Ensure we are listening to NWC updates from the relay
    105         post.send(ev, to: [url.relay], skip_ephemeral: false, delay: delay, on_flush: on_flush)
    106         return ev
    107     }
    108     
    109     @MainActor
    110     static func refresh_wallet_information(damus_state: DamusState) async {
    111         damus_state.wallet.resetWalletStateInformation()
    112         await Self.update_wallet_information(damus_state: damus_state)
    113     }
    114     
    115     @MainActor
    116     static func update_wallet_information(damus_state: DamusState) async {
    117         guard let url = damus_state.settings.nostr_wallet_connect,
    118               let nwc = WalletConnectURL(str: url) else {
    119             return
    120         }
    121         
    122         let flusher: OnFlush? = nil
    123         
    124         let delay = 0.0     // We don't need a delay when fetching a transaction list or balance
    125 
    126         WalletConnect.request_transaction_list(url: nwc, pool: damus_state.nostrNetwork.pool, post: damus_state.nostrNetwork.postbox, delay: delay, on_flush: flusher)
    127         WalletConnect.request_balance_information(url: nwc, pool: damus_state.nostrNetwork.pool, post: damus_state.nostrNetwork.postbox, delay: delay, on_flush: flusher)
    128         return
    129     }
    130 
    131     static func handle_zap_success(state: DamusState, resp: WalletConnect.FullWalletResponse) {
    132         // find the pending zap and mark it as pending-confirmed
    133         for kv in state.zaps.our_zaps {
    134             let zaps = kv.value
    135             
    136             for zap in zaps {
    137                 guard case .pending(let pzap) = zap,
    138                       case .nwc(let nwc_state) = pzap.state,
    139                       case .postbox_pending(let nwc_req) = nwc_state.state,
    140                       nwc_req.id == resp.req_id
    141                 else {
    142                     continue
    143                 }
    144                 
    145                 if nwc_state.update_state(state: .confirmed) {
    146                     // notify the zaps model of an update so it can mark them as paid
    147                     state.events.get_cache_data(NoteId(pzap.target.id)).zaps_model.objectWillChange.send()
    148                     print("NWC success confirmed")
    149                 }
    150                 
    151                 return
    152             }
    153         }
    154     }
    155 
    156     /// Send a donation zap to the Damus team
    157     static func send_donation_zap(pool: RelayPool, postbox: PostBox, nwc: WalletConnectURL, percent: Int, base_msats: Int64) async {
    158         let percent_f = Double(percent) / 100.0
    159         let donations_msats = Int64(percent_f * Double(base_msats))
    160         
    161         let payreq = LNUrlPayRequest(allowsNostr: true, commentAllowed: nil, nostrPubkey: "", callback: "https://sendsats.lol/@damus")
    162         guard let invoice = await fetch_zap_invoice(payreq, zapreq: nil, msats: donations_msats, zap_type: .non_zap, comment: nil) else {
    163             // we failed... oh well. no donation for us.
    164             print("damus-donation failed to fetch invoice")
    165             return
    166         }
    167         
    168         print("damus-donation donating...")
    169         WalletConnect.pay(url: nwc, pool: pool, post: postbox, invoice: invoice, zap_request: nil, delay: nil)
    170     }
    171 
    172     /// Handles a received Nostr Wallet Connect error
    173     static func handle_error(zapcache: Zaps, evcache: EventCache, resp: WalletConnect.FullWalletResponse) {
    174         // find a pending zap with the nwc request id associated with this response and remove it
    175         for kv in zapcache.our_zaps {
    176             let zaps = kv.value
    177             
    178             for zap in zaps {
    179                 guard case .pending(let pzap) = zap,
    180                       case .nwc(let nwc_state) = pzap.state,
    181                       case .postbox_pending(let req) = nwc_state.state,
    182                       req.id == resp.req_id
    183                 else {
    184                     continue
    185                 }
    186                 
    187                 // remove the pending zap if there was an error
    188                 let reqid = ZapRequestId(from_pending: pzap)
    189                 remove_zap(reqid: reqid, zapcache: zapcache, evcache: evcache)
    190                 return
    191             }
    192         }
    193     }
    194 }