damus

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

commit a6745af5199913aa4677d38fcd0f3e55509fa68f
parent 631220fdcb278a985217e19683b939df96a3addb
Author: William Casarin <jb55@jb55.com>
Date:   Mon, 15 May 2023 09:40:48 -0700

Implement damus zap split donations using NWC

Diffstat:
Mdamus-c/hex.h | 2+-
Mdamus/Components/ZapButton.swift | 18++++++++++++------
Mdamus/Models/HomeModel.swift | 6+++++-
Mdamus/Nostr/NostrEvent.swift | 2+-
Mdamus/Util/InsertSort.swift | 1+
Mdamus/Util/PostBox.swift | 28+++++++++++++++++++++++++---
Mdamus/Util/WalletConnect.swift | 26+++++++++++++++++++++-----
Mdamus/Util/Zap.swift | 7+++----
Mdamus/Util/Zaps.swift | 2+-
Mdamus/Views/Wallet/WalletView.swift | 2+-
10 files changed, 71 insertions(+), 23 deletions(-)

diff --git a/damus-c/hex.h b/damus-c/hex.h @@ -26,7 +26,7 @@ bool hex_decode(const char *str, size_t slen, void *buf, size_t bufsize); /** * hex_encode - Create a nul-terminated hex string * @buf: the buffer to read the data from - * @bufsize: the length of @buf + * @bufsize: the length of buf * @dest: the string to fill * @destsize: the max size of the string * diff --git a/damus/Components/ZapButton.swift b/damus/Components/ZapButton.swift @@ -208,8 +208,7 @@ func send_zap(damus_state: DamusState, event: NostrEvent, lnurl: String, is_cust return } - let zap_amount = amount_sats ?? damus_state.settings.default_zap_amount - let amount_msat = Int64(zap_amount) * 1000 + let amount_msat = Int64(amount_sats ?? damus_state.settings.default_zap_amount) * 1000 let pending_zap_state = initial_pending_zap_state(settings: damus_state.settings) let pending_zap = PendingZap(amount_msat: amount_msat, target: target, request: mzapreq, type: zap_type, state: pending_zap_state) let zapreq = mzapreq.potentially_anon_outer_request.ev @@ -239,7 +238,7 @@ func send_zap(damus_state: DamusState, event: NostrEvent, lnurl: String, is_cust damus_state.lnurls.endpoints[target.pubkey] = payreq } - guard let inv = await fetch_zap_invoice(payreq, zapreq: zapreq, sats: zap_amount, zap_type: zap_type, comment: comment) else { + guard let inv = await fetch_zap_invoice(payreq, zapreq: zapreq, msats: amount_msat, zap_type: zap_type, comment: comment) else { DispatchQueue.main.async { remove_zap(reqid: reqid, zapcache: damus_state.zaps, evcache: damus_state.events) let typ = ZappingEventType.failed(.fetching_invoice) @@ -259,9 +258,16 @@ func send_zap(damus_state: DamusState, event: NostrEvent, lnurl: String, is_cust return } - guard let nwc_req = nwc_pay(url: nwc_state.url, pool: damus_state.pool, post: damus_state.postbox, invoice: inv), - case .nwc(let pzap_state) = pending_zap_state - else { + let nwc_req = nwc_pay(url: nwc_state.url, pool: damus_state.pool, post: damus_state.postbox, invoice: inv, on_flush: .once({ pe in + + // send donation zap when the pending zap is flushed, this allows user to cancel and not send a donation + Task.init { @MainActor in + await send_donation_zap(pool: damus_state.pool, postbox: damus_state.postbox, nwc: nwc_state.url, percent: damus_state.settings.donation_percent, base_msats: amount_msat) + } + + })) + + guard let nwc_req, case .nwc(let pzap_state) = pending_zap_state else { return } diff --git a/damus/Models/HomeModel.swift b/damus/Models/HomeModel.swift @@ -154,10 +154,14 @@ class HomeModel: ObservableObject { } if resp.response.error == nil { - nwc_success(zapcache: self.damus_state.zaps, evcache: self.damus_state.events, resp: resp) + } + + guard let err = resp.response.error else { + nwc_success(state: self.damus_state, resp: resp) return } + print("nwc error: \(err)") nwc_error(zapcache: self.damus_state.zaps, evcache: self.damus_state.events, resp: resp) } } diff --git a/damus/Nostr/NostrEvent.swift b/damus/Nostr/NostrEvent.swift @@ -601,7 +601,7 @@ enum MakeZapRequest { var private_inner_request: ZapRequest { switch self { - case .priv(let _, let pzr): + case .priv(_, let pzr): return pzr.req case .normal(let zr): return zr diff --git a/damus/Util/InsertSort.swift b/damus/Util/InsertSort.swift @@ -14,6 +14,7 @@ func insert_uniq_sorted_zap(zaps: inout [Zapping], new_zap: Zapping, cmp: (Zappi if new_zap.request.id == zap.request.id { // replace pending if !new_zap.is_pending && zap.is_pending { + print("nwc: replacing pending with real zap \(new_zap.request.id)") zaps[i] = new_zap return true } diff --git a/damus/Util/PostBox.swift b/damus/Util/PostBox.swift @@ -22,16 +22,25 @@ class Relayer { } } +enum OnFlush { + case once((PostedEvent) -> Void) + case all((PostedEvent) -> Void) +} + class PostedEvent { let event: NostrEvent let skip_ephemeral: Bool var remaining: [Relayer] let flush_after: Date? + var flushed_once: Bool + let on_flush: OnFlush? - init(event: NostrEvent, remaining: [String], skip_ephemeral: Bool, flush_after: Date? = nil) { + init(event: NostrEvent, remaining: [String], skip_ephemeral: Bool, flush_after: Date?, on_flush: OnFlush?) { self.event = event self.skip_ephemeral = skip_ephemeral self.flush_after = flush_after + self.on_flush = on_flush + self.flushed_once = false self.remaining = remaining.map { Relayer(relay: $0, attempts: 0, retry_after: 2.0) } @@ -109,6 +118,19 @@ class PostBox { guard let ev = self.events[event_id] else { return false } + + if let on_flush = ev.on_flush { + switch on_flush { + case .once(let cb): + if !ev.flushed_once { + ev.flushed_once = true + cb(ev) + } + case .all(let cb): + cb(ev) + } + } + let prev_count = ev.remaining.count ev.remaining = ev.remaining.filter { $0.relay != relay_id } let after_count = ev.remaining.count @@ -132,7 +154,7 @@ class PostBox { } } - func send(_ event: NostrEvent, to: [String]? = nil, skip_ephemeral: Bool = true, delay: TimeInterval? = nil) { + func send(_ event: NostrEvent, to: [String]? = nil, skip_ephemeral: Bool = true, delay: TimeInterval? = nil, on_flush: OnFlush? = nil) { // Don't add event if we already have it if events[event.id] != nil { return @@ -140,7 +162,7 @@ class PostBox { let remaining = to ?? pool.our_descriptors.map { $0.url.id } let after = delay.map { d in Date.now.addingTimeInterval(d) } - let posted_ev = PostedEvent(event: event, remaining: remaining, skip_ephemeral: skip_ephemeral, flush_after: after) + let posted_ev = PostedEvent(event: event, remaining: remaining, skip_ephemeral: skip_ephemeral, flush_after: after, on_flush: on_flush) events[event.id] = posted_ev diff --git a/damus/Util/WalletConnect.swift b/damus/Util/WalletConnect.swift @@ -182,7 +182,8 @@ func subscribe_to_nwc(url: WalletConnectURL, pool: RelayPool) { pool.send(.subscribe(sub), to: [url.relay.id]) } -func nwc_pay(url: WalletConnectURL, pool: RelayPool, post: PostBox, invoice: String) -> NostrEvent? { +@discardableResult +func nwc_pay(url: WalletConnectURL, pool: RelayPool, post: PostBox, invoice: String, delay: TimeInterval? = 5.0, on_flush: OnFlush? = nil) -> NostrEvent? { let req = make_wallet_pay_invoice_request(invoice: invoice) guard let ev = make_wallet_connect_request(req: req, to_pk: url.pubkey, keypair: url.keypair) else { return nil @@ -190,14 +191,14 @@ func nwc_pay(url: WalletConnectURL, pool: RelayPool, post: PostBox, invoice: Str try? pool.add_relay(.nwc(url: url.relay)) subscribe_to_nwc(url: url, pool: pool) - post.send(ev, to: [url.relay.id], skip_ephemeral: false, delay: 5.0) + post.send(ev, to: [url.relay.id], skip_ephemeral: false, delay: delay, on_flush: on_flush) return ev } -func nwc_success(zapcache: Zaps, evcache: EventCache, resp: FullWalletResponse) { +func nwc_success(state: DamusState, resp: FullWalletResponse) { // find the pending zap and mark it as pending-confirmed - for kv in zapcache.our_zaps { + for kv in state.zaps.our_zaps { let zaps = kv.value for zap in zaps { @@ -211,14 +212,29 @@ func nwc_success(zapcache: Zaps, evcache: EventCache, resp: FullWalletResponse) if nwc_state.update_state(state: .confirmed) { // notify the zaps model of an update so it can mark them as paid - evcache.get_cache_data(pzap.target.id).zaps_model.objectWillChange.send() + state.events.get_cache_data(pzap.target.id).zaps_model.objectWillChange.send() print("NWC success confirmed") } + return } } } +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 + } + + nwc_pay(url: nwc, pool: pool, post: postbox, invoice: invoice, delay: nil) +} + func nwc_error(zapcache: Zaps, evcache: EventCache, resp: FullWalletResponse) { // find a pending zap with the nwc request id associated with this response and remove it for kv in zapcache.our_zaps { diff --git a/damus/Util/Zap.swift b/damus/Util/Zap.swift @@ -440,15 +440,14 @@ func fetch_static_payreq(_ lnurl: String) async -> LNUrlPayRequest? { return endpoint } -func fetch_zap_invoice(_ payreq: LNUrlPayRequest, zapreq: NostrEvent, sats: Int, zap_type: ZapType, comment: String?) async -> String? { +func fetch_zap_invoice(_ payreq: LNUrlPayRequest, zapreq: NostrEvent?, msats: Int64, zap_type: ZapType, comment: String?) async -> String? { guard var base_url = payreq.callback.flatMap({ URLComponents(string: $0) }) else { return nil } let zappable = payreq.allowsNostr ?? false - let amount: Int64 = Int64(sats) * 1000 - var query = [URLQueryItem(name: "amount", value: "\(amount)")] + var query = [URLQueryItem(name: "amount", value: "\(msats)")] if zappable && zap_type != .non_zap, let json = encode_json(zapreq) { print("zapreq json: \(json)") @@ -489,7 +488,7 @@ func fetch_zap_invoice(_ payreq: LNUrlPayRequest, zapreq: NostrEvent, sats: Int, // make sure it's the correct amount guard let bolt11 = decode_bolt11(result.pr), - .specific(amount) == bolt11.amount + .specific(msats) == bolt11.amount else { return nil } diff --git a/damus/Util/Zaps.swift b/damus/Util/Zaps.swift @@ -22,7 +22,7 @@ class Zaps { self.event_counts = [:] self.event_totals = [:] } - + func remove_zap(reqid: String) -> Zapping? { var res: Zapping? = nil for kv in our_zaps { diff --git a/damus/Views/Wallet/WalletView.swift b/damus/Views/Wallet/WalletView.swift @@ -123,7 +123,7 @@ struct WalletView: View { .foregroundColor(percent == 0 ? .gray : Color.yellow) .frame(width: 100) } - Text("Donation") + Text("💜") .foregroundColor(.white) } Spacer()