WIP Verify and respond to zap requests
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
@@ -43,7 +43,7 @@ class LnurlpayController < ApplicationController
|
||||
|
||||
# GET /lnurlpay/:username/invoice
|
||||
def invoice
|
||||
amount = params[:amount].to_i / 1000 # msats
|
||||
amount = params[:amount].to_i / 1000 # msats to sats
|
||||
comment = params[:comment] || ""
|
||||
address = @user.address
|
||||
|
||||
@@ -52,33 +52,19 @@ class LnurlpayController < ApplicationController
|
||||
return
|
||||
end
|
||||
|
||||
if !valid_comment?(comment)
|
||||
render json: { status: "ERROR", reason: "Comment too long" }
|
||||
return
|
||||
if params[:nostr].present?
|
||||
handle_zap_request amount, params[:nostr], params[:lnurl]
|
||||
else
|
||||
handle_pay_request address, amount, comment
|
||||
end
|
||||
|
||||
memo = "To #{address}"
|
||||
memo = "#{memo}: \"#{comment}\"" if comment.present?
|
||||
|
||||
payment_request = @user.ln_create_invoice({
|
||||
amount: amount, # we create invoices in sats
|
||||
memo: memo,
|
||||
description_hash: Digest::SHA2.hexdigest(metadata(address)),
|
||||
})
|
||||
|
||||
render json: {
|
||||
status: "OK",
|
||||
successAction: {
|
||||
tag: "message",
|
||||
message: "Sats received. Thank you!"
|
||||
},
|
||||
routes: [],
|
||||
pr: payment_request
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_service_available
|
||||
http_status :not_found unless Setting.lndhub_enabled?
|
||||
end
|
||||
|
||||
def find_user
|
||||
@user = User.where(cn: params[:username], ou: Setting.primary_domain).first
|
||||
http_status :not_found if @user.nil?
|
||||
@@ -88,6 +74,10 @@ class LnurlpayController < ApplicationController
|
||||
"[[\"text/identifier\", \"#{address}\"], [\"text/plain\", \"Send sats, receive thanks.\"]]"
|
||||
end
|
||||
|
||||
def zap_metadata(event)
|
||||
"[[\"application/json\", #{event.to_json}]]"
|
||||
end
|
||||
|
||||
def valid_amount?(amount_in_sats)
|
||||
amount_in_sats <= MAX_SATS && amount_in_sats >= MIN_SATS
|
||||
end
|
||||
@@ -96,9 +86,63 @@ class LnurlpayController < ApplicationController
|
||||
comment.length <= MAX_COMMENT_CHARS
|
||||
end
|
||||
|
||||
private
|
||||
def handle_pay_request(address, amount, comment)
|
||||
if !valid_comment?(comment)
|
||||
render json: { status: "ERROR", reason: "Comment too long" }
|
||||
return
|
||||
end
|
||||
|
||||
def check_service_available
|
||||
http_status :not_found unless Setting.lndhub_enabled?
|
||||
memo = "To #{address}"
|
||||
memo = "#{memo}: \"#{comment}\"" if comment.present?
|
||||
|
||||
payment_request = @user.ln_create_invoice({
|
||||
amount: amount, # sats
|
||||
memo: memo,
|
||||
description_hash: Digest::SHA2.hexdigest(metadata(address)),
|
||||
})
|
||||
|
||||
render json: {
|
||||
status: "OK",
|
||||
successAction: {
|
||||
tag: "message",
|
||||
message: "Sats received. Thank you!"
|
||||
},
|
||||
routes: [],
|
||||
pr: payment_request
|
||||
}
|
||||
end
|
||||
|
||||
def nostr_event_from_payload(nostr_param)
|
||||
event_obj = JSON.parse(nostr_param).transform_keys(&:to_sym)
|
||||
Nostr::Event.new(**event_obj)
|
||||
rescue => e
|
||||
return nil
|
||||
end
|
||||
|
||||
def valid_zap_request?(amount, event, lnurl)
|
||||
NostrManager::VerifyZapRequest.call(
|
||||
amount: amount, event: event, lnurl: lnurl
|
||||
)
|
||||
end
|
||||
|
||||
def handle_zap_request(amount, nostr_param, lnurl_param)
|
||||
event = nostr_event_from_payload(nostr_param)
|
||||
|
||||
unless event.present? && valid_zap_request?(amount*1000, event, lnurl_param)
|
||||
render json: { status: "ERROR", reason: "Invalid zap request" }
|
||||
return
|
||||
end
|
||||
|
||||
# TODO
|
||||
# raise zap_metadata(event).inspect
|
||||
|
||||
payment_request = @user.ln_create_invoice({
|
||||
amount: amount, # sats
|
||||
# TODO should be npub instead of address?
|
||||
memo: "Zapped #{@user.address} on Nostr", # TODO include event ID if given
|
||||
description_hash: Digest::SHA2.hexdigest(zap_metadata(event)),
|
||||
})
|
||||
|
||||
render json: { status: "OK", pr: payment_request }
|
||||
end
|
||||
end
|
||||
|
||||
51
app/services/nostr_manager/verify_zap_request.rb
Normal file
51
app/services/nostr_manager/verify_zap_request.rb
Normal file
@@ -0,0 +1,51 @@
|
||||
module NostrManager
|
||||
class VerifyZapRequest < NostrManagerService
|
||||
def initialize(amount:, event:, lnurl: nil)
|
||||
@amount, @event, @lnurl = amount, event, lnurl
|
||||
end
|
||||
|
||||
# https://github.com/nostr-protocol/nips/blob/27fef638e2460139cc9078427a0aec0ce4470517/57.md#appendix-d-lnurl-server-zap-request-validation
|
||||
def call
|
||||
tags = parse_tags(@event.tags)
|
||||
|
||||
@event.verify_signature &&
|
||||
@event.kind == 9734 &&
|
||||
tags.present? &&
|
||||
valid_p_tag?(tags[:p]) &&
|
||||
valid_e_tag?(tags[:e]) &&
|
||||
valid_a_tag?(tags[:a]) &&
|
||||
valid_amount_tag?(tags[:amount]) &&
|
||||
valid_lnurl_tag?(tags[:lnurl])
|
||||
end
|
||||
|
||||
def valid_p_tag?(tag)
|
||||
return false unless tag.present? && tag.length == 1
|
||||
key = Nostr::PublicKey.new(tag.first) rescue nil
|
||||
key.present?
|
||||
end
|
||||
|
||||
def valid_e_tag?(tag)
|
||||
return true unless tag.present?
|
||||
# TODO validate format of event ID properly
|
||||
tag.length == 1 && tag.first.is_a?(String)
|
||||
end
|
||||
|
||||
def valid_a_tag?(tag)
|
||||
return true unless tag.present?
|
||||
# TODO validate format of event coordinate properly
|
||||
tag.length == 1 && tag.first.is_a?(String)
|
||||
end
|
||||
|
||||
def valid_amount_tag?(tag)
|
||||
return true unless tag.present?
|
||||
amount = tag.first
|
||||
amount.is_a?(String) && amount.to_i == @amount
|
||||
end
|
||||
|
||||
def valid_lnurl_tag?(tag)
|
||||
return true unless tag.present?
|
||||
# TODO validate lnurl matching recipient's lnurlp
|
||||
tag.first.is_a?(String)
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user