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:
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()