class LnurlpayController < ApplicationController before_action :check_service_available before_action :find_user before_action :set_cors_access_control_headers MIN_SATS = 10 MAX_SATS = 1_000_000 MAX_COMMENT_CHARS = 100 # GET /.well-known/lnurlp/:username def index res = { status: "OK", callback: "https://#{Setting.accounts_domain}/lnurlpay/#{@user.cn}/invoice", tag: "payRequest", maxSendable: MAX_SATS * 1000, # msat minSendable: MIN_SATS * 1000, # msat metadata: metadata(@user.address), commentAllowed: MAX_COMMENT_CHARS } if Setting.nostr_enabled? res[:allowsNostr] = true res[:nostrPubkey] = Setting.nostr_public_key end render json: res end # GET /.well-known/keysend/:username def keysend http_status :not_found and return unless Setting.lndhub_keysend_enabled? render json: { status: "OK", tag: "keysend", pubkey: Setting.lndhub_public_key, customData: [{ customKey: "696969", customValue: @user.lndhub_username }] } end # GET /lnurlpay/:username/invoice def invoice amount = params[:amount].to_i / 1000 # msats to sats comment = params[:comment] || "" address = @user.address if !valid_amount?(amount) render json: { status: "ERROR", reason: "Invalid amount" } return end if params[:nostr].present? && Setting.nostr_enabled? handle_zap_request amount, params[:nostr], params[:lnurl] else handle_pay_request address, amount, comment end end private def set_cors_access_control_headers headers['Access-Control-Allow-Origin'] = "*" headers['Access-Control-Allow-Headers'] = "*" headers['Access-Control-Allow-Methods'] = "GET" end 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? end def metadata(address) "[[\"text/identifier\",\"#{address}\"],[\"text/plain\",\"Sats for #{address}\"]]" end def valid_amount?(amount_in_sats) amount_in_sats <= MAX_SATS && amount_in_sats >= MIN_SATS end def valid_comment?(comment) comment.length <= MAX_COMMENT_CHARS end def handle_pay_request(address, amount, comment) if !valid_comment?(comment) render json: { status: "ERROR", reason: "Comment too long" } return end desc = "To #{address}" desc = "#{desc}: \"#{comment}\"" if comment.present? invoice = LndhubManager::CreateUserInvoice.call( user: @user, payload: { amount: amount, # sats description: desc, description_hash: Digest::SHA256.hexdigest(metadata(address)), } ) render json: { status: "OK", successAction: { tag: "message", message: "Sats received. Thank you!" }, routes: [], pr: invoice["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 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? invoice = LndhubManager::CreateUserInvoice.call( user: @user, payload: { amount: amount, # sats description: desc, description_hash: Digest::SHA256.hexdigest(event.to_json), } ) @user.zaps.create! request: event, payment_request: invoice["payment_request"], amount: amount render json: { status: "OK", pr: invoice["payment_request"] } end end