WIP Persist zaps, create and send zap receipts
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing

This commit is contained in:
2024-05-09 14:31:37 +02:00
parent c0f4e7925e
commit c6c5d80fb4
17 changed files with 242 additions and 37 deletions

View File

@@ -52,7 +52,7 @@ class LnurlpayController < ApplicationController
return
end
if params[:nostr].present?
if params[:nostr].present?# TODO && Setting.nostr_enabled?
handle_zap_request amount, params[:nostr], params[:lnurl]
else
handle_pay_request address, amount, comment
@@ -131,6 +131,9 @@ class LnurlpayController < ApplicationController
return
end
# TODO might want to use the existing invoice and zap record if there are
# multiple calls with the same zap request
desc = "Zap for #{@user.address}"
desc = "#{desc}: \"#{event.content}\"" if event.content.present?
@@ -142,6 +145,10 @@ class LnurlpayController < ApplicationController
}
)
@user.zaps.create! request: event,
payment_request: invoice["payment_request"],
amount: amount
render json: { status: "OK", pr: invoice["payment_request"] }
end
end

View File

@@ -7,10 +7,14 @@ class WebhooksController < ApplicationController
def lndhub
@user = User.find_by!(ln_account: @payload[:user_login])
if @zap_request = fetch_nostr_event_from_description
NostrManager::PublishZapReceipt.call(
user: @user, zap_request: @zap_request
if zap = @user.zaps.find_by(payment_request: @payload[:payment_request])
zap_receipt = NostrManager::CreateZapReceipt.call(
zap: zap,
paid_at: Time.parse(@payload[:settled_at]).to_i,
preimage: @payload[:preimage]
)
zap.update! receipt: zap_receipt.to_h
NostrManager::PublishZapReceipt.call(zap: zap)
end
send_notifications
@@ -28,21 +32,16 @@ class WebhooksController < ApplicationController
def process_payload
@payload = JSON.parse(request.body.read, symbolize_names: true)
head :no_content and return unless @payload[:type] == "incoming"
unless @payload[:type] == "incoming" &&
@payload[:state] == "settled"
head :no_content and return
end
rescue
head :unprocessable_entity and return
end
def fetch_nostr_event_from_description
memo_json = JSON.parse(@payload[:memo])
Nostr::Event.new(**memo_json.to_h.symbolize_keys)
rescue
nil
end
def send_notifications
notify = @user.preferences[:lightning_notify_sats_received]
case notify
case @user.preferences[:lightning_notify_sats_received]
when "xmpp"
notify_xmpp
when "email"

View File

@@ -0,0 +1,7 @@
class NostrPublishEventJob < ApplicationJob
queue_as :nostr
def perform(event:, relay:)
NostrManager::PublishEvent.call(event: event, relay: relay)
end
end

View File

@@ -1,3 +1,17 @@
class Zap < ApplicationRecord
belongs_to :user
def request_event
nostr_event_from_hash(request)
end
def receipt_event
nostr_event_from_hash(receipt)
end
private
def nostr_event_from_hash(hash)
Nostr::Event.new(**hash.symbolize_keys)
end
end

View File

@@ -0,0 +1,25 @@
module NostrManager
class CreateZapReceipt < NostrManagerService
def initialize(zap:, paid_at:, preimage:)
@zap, @paid_at, @preimage = zap, paid_at, preimage
end
def call
request_tags = parse_tags(@zap.request_event.tags)
site_user.create_event(
kind: 9735,
created_at: @paid_at,
content: "",
tags: [
["p", request_tags[:p].first],
["e", request_tags[:e]&.first],
["a", request_tags[:a]&.first],
["bolt11", @zap.payment_request],
["preimage", @preimage],
["description", @zap.request_event.to_json]
].reject { |t| t[1].nil? }
)
end
end
end

View File

@@ -1,17 +1,17 @@
module NostrManager
class PublishEvent < NostrManagerService
def initialize(event: nil, relay: nil)
# @relay = relay
@relay = Nostr::Relay.new(url: 'ws://nostr-relay:7777', name: 'strfry')
keypair = Nostr::KeyPair.new(
private_key: Nostr::PrivateKey.new(Setting.nostr_private_key),
public_key: Nostr::PublicKey.new(Setting.nostr_public_key)
)
@user = Nostr::User.new(keypair: keypair)
@event = @user.create_event(
kind: Nostr::EventKind::TEXT_NOTE,
content: "The time is #{Time.now.strftime('%H:%M:%S')}"
)
def initialize(event: nil, relay_url: nil)
relay_name = relay_url.gsub(/^ws(s):\/\//, "")
@relay = Nostr::Relay.new(url: relay_url, name: relay_name)
@event = case event.class
when Nostr::Event
event
when Hash
Nostr::Event.new(**event.symbolize_keys)
# else
# TODO
# raise NotImplementedError
end
@client = Nostr::Client.new
end
@@ -25,6 +25,7 @@ module NostrManager
end
client.on :error do |e|
# TODO log relay URL/name
puts "Error: #{e}"
puts "Closing thread..."
Thread.exit
@@ -36,6 +37,7 @@ module NostrManager
if msg[0] == "OK" && msg[1] == event.id
puts "Event published. Closing thread..."
else
# TODO log relay URL/name
puts "Unexpected message from relay. Closing thread..."
end
Thread.exit

View File

@@ -1,10 +1,20 @@
module NostrManager
class PublishZapReceipt < NostrManagerService
def initialize(user:, zap_request:)
@user, @zap_request = user, zap_request
def initialize(zap:, delayed: true)
@zap, @delayed = zap, delayed
end
def call
tags = parse_tags(@zap.request_event.tags)
# TODO limit to 15 or so relays
tags[:relays].each do |relay_url|
if @delayed
NostrPublishEventJob.perform_later(event: @zap.receipt, relay: relay_url)
else
NostrManager::PublishEvent.call(event: @zap.receipt_event, relay: relay_url)
end
end
end
end
end

View File

@@ -8,4 +8,15 @@ class NostrManagerService < ApplicationService
end
out
end
def site_keypair
Nostr::KeyPair.new(
private_key: Nostr::PrivateKey.new(Setting.nostr_private_key),
public_key: Nostr::PublicKey.new(Setting.nostr_public_key)
)
end
def site_user
Nostr::User.new(keypair: site_keypair)
end
end