1
0
mirror of git://jb55.com/damus synced 2024-10-06 11:43:21 +00:00

Implement damus zap split donations using NWC

This commit is contained in:
William Casarin 2023-05-15 09:40:48 -07:00
parent 631220fdcb
commit a6745af519
10 changed files with 71 additions and 23 deletions

View File

@ -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 * hex_encode - Create a nul-terminated hex string
* @buf: the buffer to read the data from * @buf: the buffer to read the data from
* @bufsize: the length of @buf * @bufsize: the length of buf
* @dest: the string to fill * @dest: the string to fill
* @destsize: the max size of the string * @destsize: the max size of the string
* *

View File

@ -208,8 +208,7 @@ func send_zap(damus_state: DamusState, event: NostrEvent, lnurl: String, is_cust
return return
} }
let zap_amount = amount_sats ?? damus_state.settings.default_zap_amount let amount_msat = Int64(amount_sats ?? damus_state.settings.default_zap_amount) * 1000
let amount_msat = Int64(zap_amount) * 1000
let pending_zap_state = initial_pending_zap_state(settings: damus_state.settings) 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 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 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 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 { DispatchQueue.main.async {
remove_zap(reqid: reqid, zapcache: damus_state.zaps, evcache: damus_state.events) remove_zap(reqid: reqid, zapcache: damus_state.zaps, evcache: damus_state.events)
let typ = ZappingEventType.failed(.fetching_invoice) let typ = ZappingEventType.failed(.fetching_invoice)
@ -259,9 +258,16 @@ func send_zap(damus_state: DamusState, event: NostrEvent, lnurl: String, is_cust
return return
} }
guard let nwc_req = nwc_pay(url: nwc_state.url, pool: damus_state.pool, post: damus_state.postbox, invoice: inv), let nwc_req = nwc_pay(url: nwc_state.url, pool: damus_state.pool, post: damus_state.postbox, invoice: inv, on_flush: .once({ pe in
case .nwc(let pzap_state) = pending_zap_state
else { // 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 return
} }

View File

@ -154,10 +154,14 @@ class HomeModel: ObservableObject {
} }
if resp.response.error == nil { 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 return
} }
print("nwc error: \(err)")
nwc_error(zapcache: self.damus_state.zaps, evcache: self.damus_state.events, resp: resp) nwc_error(zapcache: self.damus_state.zaps, evcache: self.damus_state.events, resp: resp)
} }
} }

View File

@ -601,7 +601,7 @@ enum MakeZapRequest {
var private_inner_request: ZapRequest { var private_inner_request: ZapRequest {
switch self { switch self {
case .priv(let _, let pzr): case .priv(_, let pzr):
return pzr.req return pzr.req
case .normal(let zr): case .normal(let zr):
return zr return zr

View File

@ -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 { if new_zap.request.id == zap.request.id {
// replace pending // replace pending
if !new_zap.is_pending && zap.is_pending { if !new_zap.is_pending && zap.is_pending {
print("nwc: replacing pending with real zap \(new_zap.request.id)")
zaps[i] = new_zap zaps[i] = new_zap
return true return true
} }

View File

@ -22,16 +22,25 @@ class Relayer {
} }
} }
enum OnFlush {
case once((PostedEvent) -> Void)
case all((PostedEvent) -> Void)
}
class PostedEvent { class PostedEvent {
let event: NostrEvent let event: NostrEvent
let skip_ephemeral: Bool let skip_ephemeral: Bool
var remaining: [Relayer] var remaining: [Relayer]
let flush_after: Date? 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.event = event
self.skip_ephemeral = skip_ephemeral self.skip_ephemeral = skip_ephemeral
self.flush_after = flush_after self.flush_after = flush_after
self.on_flush = on_flush
self.flushed_once = false
self.remaining = remaining.map { self.remaining = remaining.map {
Relayer(relay: $0, attempts: 0, retry_after: 2.0) Relayer(relay: $0, attempts: 0, retry_after: 2.0)
} }
@ -109,6 +118,19 @@ class PostBox {
guard let ev = self.events[event_id] else { guard let ev = self.events[event_id] else {
return false 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 let prev_count = ev.remaining.count
ev.remaining = ev.remaining.filter { $0.relay != relay_id } ev.remaining = ev.remaining.filter { $0.relay != relay_id }
let after_count = ev.remaining.count 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 // Don't add event if we already have it
if events[event.id] != nil { if events[event.id] != nil {
return return
@ -140,7 +162,7 @@ class PostBox {
let remaining = to ?? pool.our_descriptors.map { $0.url.id } let remaining = to ?? pool.our_descriptors.map { $0.url.id }
let after = delay.map { d in Date.now.addingTimeInterval(d) } 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 events[event.id] = posted_ev

View File

@ -182,7 +182,8 @@ func subscribe_to_nwc(url: WalletConnectURL, pool: RelayPool) {
pool.send(.subscribe(sub), to: [url.relay.id]) 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) 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 { guard let ev = make_wallet_connect_request(req: req, to_pk: url.pubkey, keypair: url.keypair) else {
return nil 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)) try? pool.add_relay(.nwc(url: url.relay))
subscribe_to_nwc(url: url, pool: pool) 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 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 // 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 let zaps = kv.value
for zap in zaps { for zap in zaps {
@ -211,14 +212,29 @@ func nwc_success(zapcache: Zaps, evcache: EventCache, resp: FullWalletResponse)
if nwc_state.update_state(state: .confirmed) { if nwc_state.update_state(state: .confirmed) {
// notify the zaps model of an update so it can mark them as paid // 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") print("NWC success confirmed")
} }
return 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) { 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 // find a pending zap with the nwc request id associated with this response and remove it
for kv in zapcache.our_zaps { for kv in zapcache.our_zaps {

View File

@ -440,15 +440,14 @@ func fetch_static_payreq(_ lnurl: String) async -> LNUrlPayRequest? {
return endpoint 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 { guard var base_url = payreq.callback.flatMap({ URLComponents(string: $0) }) else {
return nil return nil
} }
let zappable = payreq.allowsNostr ?? false 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) { if zappable && zap_type != .non_zap, let json = encode_json(zapreq) {
print("zapreq json: \(json)") 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 // make sure it's the correct amount
guard let bolt11 = decode_bolt11(result.pr), guard let bolt11 = decode_bolt11(result.pr),
.specific(amount) == bolt11.amount .specific(msats) == bolt11.amount
else { else {
return nil return nil
} }

View File

@ -123,7 +123,7 @@ struct WalletView: View {
.foregroundColor(percent == 0 ? .gray : Color.yellow) .foregroundColor(percent == 0 ? .gray : Color.yellow)
.frame(width: 100) .frame(width: 100)
} }
Text("Donation") Text("💜")
.foregroundColor(.white) .foregroundColor(.white)
} }
Spacer() Spacer()