Files
damus/damus/Util/WalletConnect/WalletConnect+.swift
ericholguin 22f2aba969 nwc: Wallet Redesign
This PR redesigns the NWC wallet view. A new view is added to introduce zaps to users. The set up wallet view is simplified, with new and existing wallet setup separated.
This also adds new NWC features such as getBalance and listTransactions allowing users to see their balance and previous transactions made.

Changelog-Added: Added view introducing users to Zaps
Changelog-Added: Added new wallet view with balance and transactions list
Changelog-Changed: Improved integration with Nostr Wallet Connect wallets
Closes: https://github.com/damus-io/damus/issues/2900

Signed-off-by: ericholguin <ericholguin@apache.org>
Co-Authored-By: Daniel D’Aquino <daniel@daquino.me>
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
2025-03-19 18:00:00 -03:00

171 lines
8.3 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// WalletConnect+.swift
// damus
//
// Created by Daniel DAquino on 2023-11-27.
//
import Foundation
// TODO: Eventually we should move these convenience functions into structured classes responsible for managing this type of functionality, such as `WalletModel`
extension WalletConnect {
/// Creates and sends a subscription to an NWC relay requesting NWC responses to be sent back.
///
/// Notes: This assumes there is already a listener somewhere else
///
/// - Parameters:
/// - url: The Nostr Wallet Connect URL containing connection info to the NWC wallet
/// - pool: The RelayPool to send the subscription request through
static func subscribe(url: WalletConnectURL, pool: RelayPool) {
var filter = NostrFilter(kinds: [.nwc_response])
filter.authors = [url.pubkey]
filter.limit = 0
let sub = NostrSubscribe(filters: [filter], sub_id: "nwc")
pool.send(.subscribe(sub), to: [url.relay], skip_ephemeral: false)
}
/// Sends out a request to pay an invoice to the NWC relay, and ensures that:
/// 1. the NWC relay is connected and we are listening to NWC events
/// 2. the NWC relay is connected and we are listening to NWC
///
/// Note: This does not return information about whether the payment is succesful or not. The actual confirmation is handled elsewhere around `HomeModel` and `WalletModel`
///
/// - Parameters:
/// - url: The NWC wallet connection URL
/// - pool: The relay pool to connect to
/// - post: The postbox to send events in
/// - delay: The delay before actually sending the request to the network _(this makes it possible to cancel a zap)_
/// - on_flush: A callback to call after the event has been flushed to the network
/// - Returns: The Nostr Event that was sent to the network, representing the request that was made
@discardableResult
static func pay(url: WalletConnectURL, pool: RelayPool, post: PostBox, invoice: String, delay: TimeInterval? = 5.0, on_flush: OnFlush? = nil) -> NostrEvent? {
let req = WalletConnect.Request.payInvoice(invoice: invoice)
guard let ev = req.to_nostr_event(to_pk: url.pubkey, keypair: url.keypair) else {
return nil
}
try? pool.add_relay(.nwc(url: url.relay)) // Ensure the NWC relay is connected
WalletConnect.subscribe(url: url, pool: pool) // Ensure we are listening to NWC updates from the relay
post.send(ev, to: [url.relay], skip_ephemeral: false, delay: delay, on_flush: on_flush)
return ev
}
/// Sends out a wallet balance request to the NWC relay, and ensures that:
/// 1. the NWC relay is connected and we are listening to NWC events
/// 2. the NWC relay is connected and we are listening to NWC
///
/// Note: This does not return the actual balance information. The actual balance is handled elsewhere around `HomeModel` and `WalletModel`
///
/// - Parameters:
/// - url: The NWC wallet connection URL
/// - pool: The relay pool to connect to
/// - post: The postbox to send events in
/// - delay: The delay before actually sending the request to the network
/// - on_flush: A callback to call after the event has been flushed to the network
/// - Returns: The Nostr Event that was sent to the network, representing the request that was made
@discardableResult
static func request_balance_information(url: WalletConnectURL, pool: RelayPool, post: PostBox, delay: TimeInterval? = 0.0, on_flush: OnFlush? = nil) -> NostrEvent? {
let req = WalletConnect.Request.getBalance
guard let ev = req.to_nostr_event(to_pk: url.pubkey, keypair: url.keypair) else {
return nil
}
try? pool.add_relay(.nwc(url: url.relay)) // Ensure the NWC relay is connected
WalletConnect.subscribe(url: url, pool: pool) // Ensure we are listening to NWC updates from the relay
post.send(ev, to: [url.relay], skip_ephemeral: false, delay: delay, on_flush: on_flush)
return ev
}
/// Sends out a wallet transaction list request to the NWC relay, and ensures that:
/// 1. the NWC relay is connected and we are listening to NWC events
/// 2. the NWC relay is connected and we are listening to NWC
///
/// Note: This does not return the actual transaction list. The actual transaction list is handled elsewhere around `HomeModel` and `WalletModel`
///
/// - Parameters:
/// - url: The NWC wallet connection URL
/// - pool: The relay pool to connect to
/// - post: The postbox to send events in
/// - delay: The delay before actually sending the request to the network
/// - on_flush: A callback to call after the event has been flushed to the network
/// - Returns: The Nostr Event that was sent to the network, representing the request that was made
@discardableResult
static func request_transaction_list(url: WalletConnectURL, pool: RelayPool, post: PostBox, delay: TimeInterval? = 0.0, on_flush: OnFlush? = nil) -> NostrEvent? {
let req = WalletConnect.Request.getTransactionList(from: nil, until: nil, limit: 10, offset: 0, unpaid: false, type: "")
guard let ev = req.to_nostr_event(to_pk: url.pubkey, keypair: url.keypair) else {
return nil
}
try? pool.add_relay(.nwc(url: url.relay)) // Ensure the NWC relay is connected
WalletConnect.subscribe(url: url, pool: pool) // Ensure we are listening to NWC updates from the relay
post.send(ev, to: [url.relay], skip_ephemeral: false, delay: delay, on_flush: on_flush)
return ev
}
static func handle_zap_success(state: DamusState, resp: WalletConnect.FullWalletResponse) {
// find the pending zap and mark it as pending-confirmed
for kv in state.zaps.our_zaps {
let zaps = kv.value
for zap in zaps {
guard case .pending(let pzap) = zap,
case .nwc(let nwc_state) = pzap.state,
case .postbox_pending(let nwc_req) = nwc_state.state,
nwc_req.id == resp.req_id
else {
continue
}
if nwc_state.update_state(state: .confirmed) {
// notify the zaps model of an update so it can mark them as paid
state.events.get_cache_data(NoteId(pzap.target.id)).zaps_model.objectWillChange.send()
print("NWC success confirmed")
}
return
}
}
}
/// Send a donation zap to the Damus team
static func send_donation_zap(pool: RelayPool, postbox: PostBox, nwc: WalletConnectURL, percent: Int, base_msats: Int64) async {
let percent_f = Double(percent) / 100.0
let donations_msats = Int64(percent_f * Double(base_msats))
let payreq = LNUrlPayRequest(allowsNostr: true, commentAllowed: nil, nostrPubkey: "", callback: "https://sendsats.lol/@damus")
guard let invoice = await fetch_zap_invoice(payreq, zapreq: nil, msats: donations_msats, zap_type: .non_zap, comment: nil) else {
// we failed... oh well. no donation for us.
print("damus-donation failed to fetch invoice")
return
}
print("damus-donation donating...")
WalletConnect.pay(url: nwc, pool: pool, post: postbox, invoice: invoice, delay: nil)
}
/// Handles a received Nostr Wallet Connect error
static func handle_error(zapcache: Zaps, evcache: EventCache, resp: WalletConnect.FullWalletResponse) {
// find a pending zap with the nwc request id associated with this response and remove it
for kv in zapcache.our_zaps {
let zaps = kv.value
for zap in zaps {
guard case .pending(let pzap) = zap,
case .nwc(let nwc_state) = pzap.state,
case .postbox_pending(let req) = nwc_state.state,
req.id == resp.req_id
else {
continue
}
// remove the pending zap if there was an error
let reqid = ZapRequestId(from_pending: pzap)
remove_zap(reqid: reqid, zapcache: zapcache, evcache: evcache)
return
}
}
}
}