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:
parent
631220fdcb
commit
a6745af519
@ -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
|
||||||
*
|
*
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
|
Loading…
Reference in New Issue
Block a user