From dbc811b8405c82fd6a2abe80c87137304b38d1c7 Mon Sep 17 00:00:00 2001 From: Sebastian Kippe Date: Mon, 22 Nov 2021 15:41:05 -0600 Subject: [PATCH] Add LndHub service, lnurl-pay endpoints Enables the lnurl-pay payment workflow --- app/controllers/lnurlpay_controller.rb | 58 ++++++++++++++++++++++ app/controllers/wallet_controller.rb | 23 +++------ app/helpers/lnurlpay_helper.rb | 2 + app/jobs/create_lndhub_wallet_job.rb | 8 +-- app/models/user.rb | 6 +++ app/services/lndhub.rb | 57 +++++++++++++++++++++ config/routes.rb | 3 ++ spec/helpers/lnurlpay_helper_spec.rb | 15 ++++++ spec/jobs/create_lndhub_wallet_job_spec.rb | 2 +- spec/requests/lnurlpay_spec.rb | 7 +++ 10 files changed, 158 insertions(+), 23 deletions(-) create mode 100644 app/controllers/lnurlpay_controller.rb create mode 100644 app/helpers/lnurlpay_helper.rb create mode 100644 app/services/lndhub.rb create mode 100644 spec/helpers/lnurlpay_helper_spec.rb create mode 100644 spec/requests/lnurlpay_spec.rb diff --git a/app/controllers/lnurlpay_controller.rb b/app/controllers/lnurlpay_controller.rb new file mode 100644 index 0000000..c9e3b26 --- /dev/null +++ b/app/controllers/lnurlpay_controller.rb @@ -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 diff --git a/app/controllers/wallet_controller.rb b/app/controllers/wallet_controller.rb index c74a4f3..67d6abe 100644 --- a/app/controllers/wallet_controller.rb +++ b/app/controllers/wallet_controller.rb @@ -27,27 +27,18 @@ class WalletController < ApplicationController if session["ln_auth_token"].present? @ln_auth_token = session["ln_auth_token"] else - res = Faraday.post("#{ENV["LNDHUB_API_URL"]}/auth?type=auth", - { login: current_user.ln_login, password: current_user.ln_password }.to_json, - "Content-Type" => "application/json", - "Accept" => "application/json") - - credentials = JSON.parse(res.body) - session["ln_auth_token"] = credentials["access_token"] - @ln_auth_token = credentials["access_token"] + lndhub = Lndhub.new + auth_token = lndhub.authenticate(current_user) + session["ln_auth_token"] = auth_token + @ln_auth_token = auth_token end rescue # TODO add exception tracking end def fetch_balance - res = Faraday.get("#{ENV["LNDHUB_API_URL"]}/balance", {}, { - "Content-Type" => "application/json", - "Accept" => "application/json", - "Authorization" => "Bearer #{@ln_auth_token}" - }) - - data = JSON.parse(res.body) - data["BTC"]["AvailableBalance"] + lndhub = Lndhub.new + data = lndhub.balance @ln_auth_token + data["BTC"]["AvailableBalance"] end end diff --git a/app/helpers/lnurlpay_helper.rb b/app/helpers/lnurlpay_helper.rb new file mode 100644 index 0000000..45170fd --- /dev/null +++ b/app/helpers/lnurlpay_helper.rb @@ -0,0 +1,2 @@ +module LnurlpayHelper +end diff --git a/app/jobs/create_lndhub_wallet_job.rb b/app/jobs/create_lndhub_wallet_job.rb index ff283ef..74afeaa 100644 --- a/app/jobs/create_lndhub_wallet_job.rb +++ b/app/jobs/create_lndhub_wallet_job.rb @@ -2,14 +2,10 @@ class CreateLndhubWalletJob < ApplicationJob queue_as :default def perform(user) - res = Faraday.post("#{ENV["LNDHUB_API_URL"]}/create", - { partnerid: "bluewallet", accounttype: "common" }.to_json, - "Content-Type" => "application/json") - - credentials = JSON.parse(res.body) + lndhub = Lndhub.new + credentials = lndhub.create({ partnerid: user.ou, accounttype: "user" }) user.update! ln_login: credentials["login"], ln_password: credentials["password"] end - end diff --git a/app/models/user.rb b/app/models/user.rb index e038118..0a0261d 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -56,4 +56,10 @@ class User < ApplicationRecord self.valid? self.errors[attribute_name].blank? end + + def ln_create_invoice(payload) + lndhub = Lndhub.new + lndhub.authenticate self + lndhub.addinvoice payload + end end diff --git a/app/services/lndhub.rb b/app/services/lndhub.rb new file mode 100644 index 0000000..5706148 --- /dev/null +++ b/app/services/lndhub.rb @@ -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 diff --git a/config/routes.rb b/config/routes.rb index e1af6f7..8843058 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -21,6 +21,9 @@ Rails.application.routes.draw do 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 root to: 'dashboard#index' get 'invitations', to: 'invitations#index' diff --git a/spec/helpers/lnurlpay_helper_spec.rb b/spec/helpers/lnurlpay_helper_spec.rb new file mode 100644 index 0000000..f00ab6b --- /dev/null +++ b/spec/helpers/lnurlpay_helper_spec.rb @@ -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 diff --git a/spec/jobs/create_lndhub_wallet_job_spec.rb b/spec/jobs/create_lndhub_wallet_job_spec.rb index 68aecd9..e11ab2c 100644 --- a/spec/jobs/create_lndhub_wallet_job_spec.rb +++ b/spec/jobs/create_lndhub_wallet_job_spec.rb @@ -16,7 +16,7 @@ RSpec.describe CreateLndhubWalletJob, type: :job do perform_enqueued_jobs { job } 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 expect(user.ln_login).to eq("abc123") diff --git a/spec/requests/lnurlpay_spec.rb b/spec/requests/lnurlpay_spec.rb new file mode 100644 index 0000000..d5b81c1 --- /dev/null +++ b/spec/requests/lnurlpay_spec.rb @@ -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