Merge pull request 'Add Webhooks and XMPP notifications for incoming sats' (#79) from feature/webhooks into master
continuous-integration/drone/push Build is passing Details

Reviewed-on: #79
Reviewed-by: bumi <bumi@noreply.kosmos.org>
This commit is contained in:
Râu Cao 2023-01-13 04:33:02 +00:00
commit f62e49f524
17 changed files with 195 additions and 5 deletions

View File

@ -9,3 +9,5 @@ BTCPAY_API_URL='http://localhost:23001/api/v1'
LNDHUB_API_URL='http://localhost:3023'
LNDHUB_PUBLIC_URL='https://lndhub.kosmos.org'
WEBHOOKS_ALLOWED_IPS='10.1.1.163'

View File

@ -5,3 +5,5 @@ BTCPAY_API_URL='http://10.1.1.163:23001/api/v1'
LNDHUB_LEGACY_API_URL='http://10.1.1.163:3026'
LNDHUB_API_URL='http://10.1.1.163:3026'
LNDHUB_PUBLIC_URL='https://lndhub.kosmos.org'
WEBHOOKS_ALLOWED_IPS='10.1.1.163'

View File

@ -1,5 +1,9 @@
EJABBERD_API_URL='http://xmpp.example.com/api'
BTCPAY_API_URL='http://btcpay.example.com/api/v1'
LNDHUB_LEGACY_API_URL='http://localhost:3023'
LNDHUB_API_URL='http://localhost:3026'
LNDHUB_PUBLIC_URL='https://lndhub.kosmos.org'
WEBHOOKS_ALLOWED_IPS='10.1.1.23'

View File

@ -0,0 +1,40 @@
class WebhooksController < ApplicationController
skip_forgery_protection
before_action :authorize_request
def lndhub
begin
payload = JSON.parse(request.body.read, symbolize_names: true)
head :no_content and return unless payload[:type] == "incoming"
rescue
head :unprocessable_entity and return
end
user = User.find_by!(ln_account: payload[:user_login])
# TODO make configurable
notify_xmpp(user.address, payload[:amount], payload[:memo])
head :ok
end
private
def notify_xmpp(address, amt_sats, memo)
payload = {
type: "normal",
from: "kosmos.org", # TODO domain config
to: address,
subject: "Sats received!",
body: "#{amt_sats} sats received in your wallet. Memo: \"#{memo}\""
}
XmppSendMessageJob.perform_later(payload)
end
def authorize_request
if !ENV['WEBHOOKS_ALLOWED_IPS'].split(',').include?(request.remote_ip)
head :forbidden and return
end
end
end

View File

@ -7,7 +7,8 @@ class CreateLndhubAccountJob < ApplicationJob
lndhub = LndhubV2.new
credentials = lndhub.create_account
user.update! ln_login: credentials["login"],
user.update! ln_account: credentials["login"],
ln_login: credentials["login"], # TODO remove when production is migrated
ln_password: credentials["password"]
end
end

View File

@ -1,4 +1,4 @@
class ExchangeXmppContactsJob < ApplicationJob
class XmppExchangeContactsJob < ApplicationJob
queue_as :default
def perform(inviter, username, domain)

View File

@ -0,0 +1,8 @@
class XmppSendMessageJob < ApplicationJob
queue_as :default
def perform(payload)
ejabberd = EjabberdApiClient.new
ejabberd.send_message payload
end
end

View File

@ -46,7 +46,7 @@ class CreateAccount < ApplicationService
def exchange_xmpp_contacts
#TODO enable in development when we have easy setup of ejabberd etc.
return if Rails.env.development?
ExchangeXmppContactsJob.perform_later(@invitation.user, @username, @domain)
XmppExchangeContactsJob.perform_later(@invitation.user, @username, @domain)
end
def create_lndhub_account(user)

View File

@ -17,4 +17,8 @@ class EjabberdApiClient
def add_rosteritem(payload)
post "add_rosteritem", payload
end
def send_message(payload)
post "send_message", payload
end
end

View File

@ -25,6 +25,8 @@ Rails.application.routes.draw do
get 'lnurlpay/:address', to: 'lnurlpay#index', constraints: { address: /[^\/]+/}
get 'lnurlpay/:address/invoice', to: 'lnurlpay#invoice', constraints: { address: /[^\/]+/}
post 'webhooks/lndhub', to: 'webhooks#lndhub'
namespace :api do
get 'kredits/onchain_btc_balance', to: 'kredits#onchain_btc_balance'
end

View File

@ -0,0 +1,9 @@
class AddLnAccountToUsers < ActiveRecord::Migration[7.0]
def change
add_column :users, :ln_account, :string
User.all.each do |user|
user.update! ln_account: user.ln_login
end
end
end

View File

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.0].define(version: 2021_11_20_010540) do
ActiveRecord::Schema[7.0].define(version: 2023_01_11_113139) do
create_table "donations", force: :cascade do |t|
t.integer "user_id"
t.integer "amount_sats"
@ -48,6 +48,7 @@ ActiveRecord::Schema[7.0].define(version: 2021_11_20_010540) do
t.string "unconfirmed_email"
t.text "ln_login_ciphertext"
t.text "ln_password_ciphertext"
t.string "ln_account"
t.index ["email"], name: "index_users_on_email", unique: true
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
end

View File

@ -2,8 +2,10 @@ FactoryBot.define do
factory :user do
id { 1 }
cn { "jimmy" }
ou { "kosmos.org" }
email { "jimmy@example.com" }
password { "dis-muh-password" }
confirmed_at { DateTime.now }
ln_account { "123456" }
end
end

19
spec/fixtures/lndhub/incoming.json vendored Normal file
View File

@ -0,0 +1,19 @@
{
"id": 58,
"type": "incoming",
"user_login": "123456abcdef",
"amount": 12300,
"fee": 0,
"memo": "Buy you some beers",
"description_hash": "106af234beebd478206535486051b4f212bd31d2ed0f93e3efce7b5e7603d743",
"payment_request": "lnbc1u1p3mull3pp5qw4x46ew6kjknudypyjsg8maw935tr5kkuz7t6h7pugp3pt4msyqhp5zp40yd97a028sgr9x4yxq5d57gft6vwja58e8cl0eea4uasr6apscqzpgxqyz5vqsp53m2n8h6yeflgukv5fhwm802kur6un9w8nvycl7auk67w5g2u008q9qyyssqml8rfmxyvp32qd5939qx7uu0w6ppjuujlpwsrz28m9u0dzp799hz5j72w0xm8pg97hd4hdvwh9zxaw2hewnnmzewvc550f9y3qsfaegphmk0mu",
"destination_pubkey_hex": "024cd3be18617f39cf645851e3ba63f51fc13f0bb09e3bb25e6fd4de556486d946",
"r_hash": "03aa6aeb2ed5a569f1a40925041f7d7163458e96b705e5eafe0f10188575dc08",
"preimage": "3539663535656537343331663432653165396430623966633664656664646563",
"keysend": false,
"state": "settled",
"created_at": "2023-01-11T09:22:57.546364Z",
"expires_at": "2023-01-12T09:22:57.547209Z",
"updated_at": "2023-01-11T09:22:58.046236131Z",
"settled_at": "2023-01-11T09:22:58.046232174Z"
}

19
spec/fixtures/lndhub/outgoing.json vendored Normal file
View File

@ -0,0 +1,19 @@
{
"id": 59,
"type": "outgoing",
"user_login": "123456abcdef",
"amount": 12400,
"fee": 10,
"memo": "Top up mobile phone",
"description_hash": "106af234beebd478206535486051b4f212bd31d2ed0f93e3efce7b5e7603d743",
"payment_request": "lnbc1u1p3mull3pp5qw4x46ew6kjknudypyjsg8maw935tr5kkuz7t6h7pugp3pt4msyqhp5zp40yd97a028sgr9x4yxq5d57gft6vwja58e8cl0eea4uasr6apscqzpgxqyz5vqsp53m2n8h6yeflgukv5fhwm802kur6un9w8nvycl7auk67w5g2u008q9qyyssqml8rfmxyvp32qd5939qx7uu0w6ppjuujlpwsrz28m9u0dzp799hz5j72w0xm8pg97hd4hdvwh9zxaw2hewnnmzewvc550f9y3qsfaegphmk0mu",
"destination_pubkey_hex": "024cd3be18617f39cf645851e3ba63f51fc13f0bb09e3bb25e6fd4de556486d946",
"r_hash": "03aa6aeb2ed5a569f1a40925041f7d7163458e96b705e5eafe0f10188575dc08",
"preimage": "3539663535656537343331663432653165396430623966633664656664646563",
"keysend": false,
"state": "settled",
"created_at": "2023-01-11T09:22:57.546364Z",
"expires_at": "2023-01-12T09:22:57.547209Z",
"updated_at": "2023-01-11T09:22:58.046236131Z",
"settled_at": "2023-01-11T09:22:58.046232174Z"
}

View File

@ -1,7 +1,7 @@
require 'rails_helper'
require 'webmock/rspec'
RSpec.describe ExchangeXmppContactsJob, type: :job do
RSpec.describe XmppExchangeContactsJob, type: :job do
let(:user) { create :user, cn: "willherschel", ou: "kosmos.org" }
subject(:job) {

View File

@ -0,0 +1,77 @@
require 'rails_helper'
RSpec.describe "Webhooks", type: :request do
describe "Allowed IP addresses" do
context "IP not allowed" do
it "returns a 403 status" do
post "/webhooks/lndhub"
expect(response).to have_http_status(:forbidden)
end
end
context "IP allowed" do
before do
ENV['WEBHOOKS_ALLOWED_IPS'] = '127.0.0.1'
end
it "does not return a 403 status" do
post "/webhooks/lndhub"
expect(response).not_to have_http_status(:forbidden)
end
end
end
# Webhooks from lndhub.go
describe "/webhooks/lndhub" do
before do
ENV['WEBHOOKS_ALLOWED_IPS'] = '127.0.0.1'
end
describe "Payload cannot be processed as JSON" do
before do
post "/webhooks/lndhub", params: "Foo"
end
it "returns a 422 status" do
expect(response).to have_http_status(:unprocessable_entity)
end
end
describe "Valid payload for outgoing payment" do
let(:payload) { JSON.parse(File.read(File.expand_path("../fixtures/lndhub/outgoing.json", File.dirname(__FILE__)))) }
before do
post "/webhooks/lndhub", params: payload.to_json
end
it "returns a 204 status" do
expect(response).to have_http_status(:no_content)
end
end
describe "Valid payload for incoming payment" do
let(:user) { create :user, ln_account: "123456abcdef" }
let(:payload) { JSON.parse(File.read(File.expand_path("../fixtures/lndhub/incoming.json", File.dirname(__FILE__)))) }
before do
user.save! #FIXME this should not be necessary
post "/webhooks/lndhub", params: payload.to_json
end
it "returns a 200 status" do
expect(response).to have_http_status(:ok)
end
it "sends an XMPP message to the account owner's JID" do
expect(enqueued_jobs.size).to eq(1)
msg = enqueued_jobs.first['arguments'].first
expect(msg["type"]).to eq('normal')
expect(msg["from"]).to eq('kosmos.org')
expect(msg["to"]).to eq(user.address)
expect(msg["subject"]).to eq('Sats received!')
expect(msg["body"]).to match(/^12300 sats received/)
end
end
end
end