Add LndHub service, lnurl-pay endpoints
Enables the lnurl-pay payment workflow
This commit is contained in:
		
							parent
							
								
									884070a3cb
								
							
						
					
					
						commit
						dbc811b840
					
				
							
								
								
									
										58
									
								
								app/controllers/lnurlpay_controller.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								app/controllers/lnurlpay_controller.rb
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,58 @@
 | 
				
			|||||||
 | 
					class LnurlpayController < ApplicationController
 | 
				
			||||||
 | 
					  before_action :find_user_by_address
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def index
 | 
				
			||||||
 | 
					    render json: {
 | 
				
			||||||
 | 
					      status: "OK",
 | 
				
			||||||
 | 
					      callback: "https://accounts.kosmos.org/lnurlpay/#{@user.address}/invoice",
 | 
				
			||||||
 | 
					      tag: "payRequest",
 | 
				
			||||||
 | 
					      maxSendable: 1000000,
 | 
				
			||||||
 | 
					      minSendable: 1000,
 | 
				
			||||||
 | 
					      metadata: metadata(@user.address),
 | 
				
			||||||
 | 
					      commentAllowed: 255
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def invoice
 | 
				
			||||||
 | 
					    amount = params[:amount].to_i # msats
 | 
				
			||||||
 | 
					    address = params[:address]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    validate_amount(amount)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # amount = amount / 1000 # we need sats
 | 
				
			||||||
 | 
					    payment_request = @user.ln_create_invoice({
 | 
				
			||||||
 | 
					      amount: amount,
 | 
				
			||||||
 | 
					      description_hash: Digest::SHA2.hexdigest(metadata(address))
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    render json: {
 | 
				
			||||||
 | 
					      status: "OK",
 | 
				
			||||||
 | 
					      successAction: {
 | 
				
			||||||
 | 
					        tag: "message",
 | 
				
			||||||
 | 
					        message: "Payment received. Thanks!"
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      routes: [],
 | 
				
			||||||
 | 
					      pr: payment_request
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def find_user_by_address
 | 
				
			||||||
 | 
					    address = params[:address].split("@")
 | 
				
			||||||
 | 
					    @user = User.where(cn: address.first, ou: address.last).first
 | 
				
			||||||
 | 
					    http_status :not_found if @user.nil?
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def metadata(address)
 | 
				
			||||||
 | 
					    "[[\"text/identifier\", \"#{address}\"], [\"text/plain\", \"Sats for #{address}\"]]"
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def validate_amount(amount)
 | 
				
			||||||
 | 
					    if amount > 1000000 || amount < 1000
 | 
				
			||||||
 | 
					      render json: { status: "ERROR", reason: "Invalid amount" }
 | 
				
			||||||
 | 
					      return
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
@ -27,27 +27,18 @@ class WalletController < ApplicationController
 | 
				
			|||||||
    if session["ln_auth_token"].present?
 | 
					    if session["ln_auth_token"].present?
 | 
				
			||||||
      @ln_auth_token = session["ln_auth_token"]
 | 
					      @ln_auth_token = session["ln_auth_token"]
 | 
				
			||||||
    else
 | 
					    else
 | 
				
			||||||
      res = Faraday.post("#{ENV["LNDHUB_API_URL"]}/auth?type=auth",
 | 
					      lndhub = Lndhub.new
 | 
				
			||||||
                         { login: current_user.ln_login, password: current_user.ln_password }.to_json,
 | 
					      auth_token = lndhub.authenticate(current_user)
 | 
				
			||||||
                         "Content-Type" => "application/json",
 | 
					      session["ln_auth_token"] = auth_token
 | 
				
			||||||
                         "Accept" => "application/json")
 | 
					      @ln_auth_token = auth_token
 | 
				
			||||||
 | 
					 | 
				
			||||||
      credentials = JSON.parse(res.body)
 | 
					 | 
				
			||||||
      session["ln_auth_token"] = credentials["access_token"]
 | 
					 | 
				
			||||||
      @ln_auth_token = credentials["access_token"]
 | 
					 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  rescue
 | 
					  rescue
 | 
				
			||||||
    # TODO add exception tracking
 | 
					    # TODO add exception tracking
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def fetch_balance
 | 
					  def fetch_balance
 | 
				
			||||||
      res = Faraday.get("#{ENV["LNDHUB_API_URL"]}/balance", {}, {
 | 
					    lndhub = Lndhub.new
 | 
				
			||||||
        "Content-Type" => "application/json",
 | 
					    data = lndhub.balance @ln_auth_token
 | 
				
			||||||
        "Accept" => "application/json",
 | 
					    data["BTC"]["AvailableBalance"]
 | 
				
			||||||
        "Authorization" => "Bearer #{@ln_auth_token}"
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      data = JSON.parse(res.body)
 | 
					 | 
				
			||||||
      data["BTC"]["AvailableBalance"]
 | 
					 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										2
									
								
								app/helpers/lnurlpay_helper.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								app/helpers/lnurlpay_helper.rb
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,2 @@
 | 
				
			|||||||
 | 
					module LnurlpayHelper
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
@ -2,14 +2,10 @@ class CreateLndhubWalletJob < ApplicationJob
 | 
				
			|||||||
  queue_as :default
 | 
					  queue_as :default
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def perform(user)
 | 
					  def perform(user)
 | 
				
			||||||
    res = Faraday.post("#{ENV["LNDHUB_API_URL"]}/create",
 | 
					    lndhub = Lndhub.new
 | 
				
			||||||
                       { partnerid: "bluewallet", accounttype: "common" }.to_json,
 | 
					    credentials = lndhub.create({ partnerid: user.ou, accounttype: "user" })
 | 
				
			||||||
                       "Content-Type" => "application/json")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    credentials = JSON.parse(res.body)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    user.update! ln_login: credentials["login"],
 | 
					    user.update! ln_login: credentials["login"],
 | 
				
			||||||
                 ln_password: credentials["password"]
 | 
					                 ln_password: credentials["password"]
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 | 
				
			|||||||
@ -56,4 +56,10 @@ class User < ApplicationRecord
 | 
				
			|||||||
    self.valid?
 | 
					    self.valid?
 | 
				
			||||||
    self.errors[attribute_name].blank?
 | 
					    self.errors[attribute_name].blank?
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def ln_create_invoice(payload)
 | 
				
			||||||
 | 
					    lndhub = Lndhub.new
 | 
				
			||||||
 | 
					    lndhub.authenticate self
 | 
				
			||||||
 | 
					    lndhub.addinvoice payload
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										57
									
								
								app/services/lndhub.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								app/services/lndhub.rb
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,57 @@
 | 
				
			|||||||
 | 
					class Lndhub
 | 
				
			||||||
 | 
					  attr_accessor :auth_token
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def initialize
 | 
				
			||||||
 | 
					    @base_url = ENV["LNDHUB_API_URL"]
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def post(endpoint, payload)
 | 
				
			||||||
 | 
					    headers = { "Content-Type" => "application/json" }
 | 
				
			||||||
 | 
					    if auth_token
 | 
				
			||||||
 | 
					      headers.merge!({ "Authorization" => "Bearer #{auth_token}" })
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    res = Faraday.post "#{@base_url}/#{endpoint}", payload.to_json, headers
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if res.status != 200
 | 
				
			||||||
 | 
					      Rails.logger.error "[lndhub] API request failed:"
 | 
				
			||||||
 | 
					      Rails.logger.error res.body
 | 
				
			||||||
 | 
					      #TODO add some kind of exception tracking/notifications
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    JSON.parse(res.body)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def get(endpoint, auth_token)
 | 
				
			||||||
 | 
					    res = Faraday.get("#{@base_url}/#{endpoint}", {}, {
 | 
				
			||||||
 | 
					      "Content-Type" => "application/json",
 | 
				
			||||||
 | 
					      "Accept" => "application/json",
 | 
				
			||||||
 | 
					      "Authorization" => "Bearer #{auth_token}"
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    JSON.parse(res.body)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def create(payload)
 | 
				
			||||||
 | 
					    post "create", payload
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def authenticate(user)
 | 
				
			||||||
 | 
					    credentials = post "auth?type=auth", { login: user.ln_login, password: user.ln_password }
 | 
				
			||||||
 | 
					    self.auth_token = credentials["access_token"]
 | 
				
			||||||
 | 
					    self.auth_token
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def balance(user_token)
 | 
				
			||||||
 | 
					    get "balance", user_token || auth_token
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def addinvoice(payload)
 | 
				
			||||||
 | 
					    invoice = post "addinvoice", {
 | 
				
			||||||
 | 
					      amt: payload[:amount],
 | 
				
			||||||
 | 
					      description_hash: payload[:description_hash]
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    invoice["payment_request"]
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
@ -21,6 +21,9 @@ Rails.application.routes.draw do
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  get 'wallet', to: 'wallet#index'
 | 
					  get 'wallet', to: 'wallet#index'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  get 'lnurlpay/:address', to: 'lnurlpay#index', constraints: { address: /[^\/]+/}
 | 
				
			||||||
 | 
					  get 'lnurlpay/:address/invoice', to: 'lnurlpay#invoice', constraints: { address: /[^\/]+/}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  namespace :admin do
 | 
					  namespace :admin do
 | 
				
			||||||
    root to: 'dashboard#index'
 | 
					    root to: 'dashboard#index'
 | 
				
			||||||
    get 'invitations', to: 'invitations#index'
 | 
					    get 'invitations', to: 'invitations#index'
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										15
									
								
								spec/helpers/lnurlpay_helper_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								spec/helpers/lnurlpay_helper_spec.rb
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,15 @@
 | 
				
			|||||||
 | 
					require 'rails_helper'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Specs in this file have access to a helper object that includes
 | 
				
			||||||
 | 
					# the LnurlpayHelper. For example:
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# describe LnurlpayHelper do
 | 
				
			||||||
 | 
					#   describe "string concat" do
 | 
				
			||||||
 | 
					#     it "concats two strings with spaces" do
 | 
				
			||||||
 | 
					#       expect(helper.concat_strings("this","that")).to eq("this that")
 | 
				
			||||||
 | 
					#     end
 | 
				
			||||||
 | 
					#   end
 | 
				
			||||||
 | 
					# end
 | 
				
			||||||
 | 
					RSpec.describe LnurlpayHelper, type: :helper do
 | 
				
			||||||
 | 
					  pending "add some examples to (or delete) #{__FILE__}"
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
@ -16,7 +16,7 @@ RSpec.describe CreateLndhubWalletJob, type: :job do
 | 
				
			|||||||
    perform_enqueued_jobs { job }
 | 
					    perform_enqueued_jobs { job }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    expect(WebMock).to have_requested(:post, "http://10.1.1.163:3023/create")
 | 
					    expect(WebMock).to have_requested(:post, "http://10.1.1.163:3023/create")
 | 
				
			||||||
      .with { |req| req.body == '{"partnerid":"bluewallet","accounttype":"common"}' }
 | 
					      .with { |req| req.body == '{"partnerid":"kosmos.org","accounttype":"user"}' }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    user.reload
 | 
					    user.reload
 | 
				
			||||||
    expect(user.ln_login).to eq("abc123")
 | 
					    expect(user.ln_login).to eq("abc123")
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										7
									
								
								spec/requests/lnurlpay_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								spec/requests/lnurlpay_spec.rb
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,7 @@
 | 
				
			|||||||
 | 
					require 'rails_helper'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					RSpec.describe "Lnurlpays", type: :request do
 | 
				
			||||||
 | 
					  describe "GET /index" do
 | 
				
			||||||
 | 
					    pending "add some examples (or delete) #{__FILE__}"
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user