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 }