Merge pull request 'Add Webhooks and XMPP notifications for incoming sats' (#79) from feature/webhooks into master
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #79 Reviewed-by: bumi <bumi@noreply.kosmos.org>
This commit is contained in:
commit
f62e49f524
@ -9,3 +9,5 @@ BTCPAY_API_URL='http://localhost:23001/api/v1'
|
|||||||
|
|
||||||
LNDHUB_API_URL='http://localhost:3023'
|
LNDHUB_API_URL='http://localhost:3023'
|
||||||
LNDHUB_PUBLIC_URL='https://lndhub.kosmos.org'
|
LNDHUB_PUBLIC_URL='https://lndhub.kosmos.org'
|
||||||
|
|
||||||
|
WEBHOOKS_ALLOWED_IPS='10.1.1.163'
|
||||||
|
@ -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_LEGACY_API_URL='http://10.1.1.163:3026'
|
||||||
LNDHUB_API_URL='http://10.1.1.163:3026'
|
LNDHUB_API_URL='http://10.1.1.163:3026'
|
||||||
LNDHUB_PUBLIC_URL='https://lndhub.kosmos.org'
|
LNDHUB_PUBLIC_URL='https://lndhub.kosmos.org'
|
||||||
|
|
||||||
|
WEBHOOKS_ALLOWED_IPS='10.1.1.163'
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
EJABBERD_API_URL='http://xmpp.example.com/api'
|
EJABBERD_API_URL='http://xmpp.example.com/api'
|
||||||
|
|
||||||
BTCPAY_API_URL='http://btcpay.example.com/api/v1'
|
BTCPAY_API_URL='http://btcpay.example.com/api/v1'
|
||||||
|
|
||||||
LNDHUB_LEGACY_API_URL='http://localhost:3023'
|
LNDHUB_LEGACY_API_URL='http://localhost:3023'
|
||||||
LNDHUB_API_URL='http://localhost:3026'
|
LNDHUB_API_URL='http://localhost:3026'
|
||||||
LNDHUB_PUBLIC_URL='https://lndhub.kosmos.org'
|
LNDHUB_PUBLIC_URL='https://lndhub.kosmos.org'
|
||||||
|
|
||||||
|
WEBHOOKS_ALLOWED_IPS='10.1.1.23'
|
||||||
|
40
app/controllers/webhooks_controller.rb
Normal file
40
app/controllers/webhooks_controller.rb
Normal 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
|
@ -7,7 +7,8 @@ class CreateLndhubAccountJob < ApplicationJob
|
|||||||
lndhub = LndhubV2.new
|
lndhub = LndhubV2.new
|
||||||
credentials = lndhub.create_account
|
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"]
|
ln_password: credentials["password"]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
class ExchangeXmppContactsJob < ApplicationJob
|
class XmppExchangeContactsJob < ApplicationJob
|
||||||
queue_as :default
|
queue_as :default
|
||||||
|
|
||||||
def perform(inviter, username, domain)
|
def perform(inviter, username, domain)
|
8
app/jobs/xmpp_send_message_job.rb
Normal file
8
app/jobs/xmpp_send_message_job.rb
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
class XmppSendMessageJob < ApplicationJob
|
||||||
|
queue_as :default
|
||||||
|
|
||||||
|
def perform(payload)
|
||||||
|
ejabberd = EjabberdApiClient.new
|
||||||
|
ejabberd.send_message payload
|
||||||
|
end
|
||||||
|
end
|
@ -46,7 +46,7 @@ class CreateAccount < ApplicationService
|
|||||||
def exchange_xmpp_contacts
|
def exchange_xmpp_contacts
|
||||||
#TODO enable in development when we have easy setup of ejabberd etc.
|
#TODO enable in development when we have easy setup of ejabberd etc.
|
||||||
return if Rails.env.development?
|
return if Rails.env.development?
|
||||||
ExchangeXmppContactsJob.perform_later(@invitation.user, @username, @domain)
|
XmppExchangeContactsJob.perform_later(@invitation.user, @username, @domain)
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_lndhub_account(user)
|
def create_lndhub_account(user)
|
||||||
|
@ -17,4 +17,8 @@ class EjabberdApiClient
|
|||||||
def add_rosteritem(payload)
|
def add_rosteritem(payload)
|
||||||
post "add_rosteritem", payload
|
post "add_rosteritem", payload
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def send_message(payload)
|
||||||
|
post "send_message", payload
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -25,6 +25,8 @@ Rails.application.routes.draw do
|
|||||||
get 'lnurlpay/:address', to: 'lnurlpay#index', constraints: { address: /[^\/]+/}
|
get 'lnurlpay/:address', to: 'lnurlpay#index', constraints: { address: /[^\/]+/}
|
||||||
get 'lnurlpay/:address/invoice', to: 'lnurlpay#invoice', constraints: { address: /[^\/]+/}
|
get 'lnurlpay/:address/invoice', to: 'lnurlpay#invoice', constraints: { address: /[^\/]+/}
|
||||||
|
|
||||||
|
post 'webhooks/lndhub', to: 'webhooks#lndhub'
|
||||||
|
|
||||||
namespace :api do
|
namespace :api do
|
||||||
get 'kredits/onchain_btc_balance', to: 'kredits#onchain_btc_balance'
|
get 'kredits/onchain_btc_balance', to: 'kredits#onchain_btc_balance'
|
||||||
end
|
end
|
||||||
|
9
db/migrate/20230111113139_add_ln_account_to_users.rb
Normal file
9
db/migrate/20230111113139_add_ln_account_to_users.rb
Normal 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
|
@ -10,7 +10,7 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# 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|
|
create_table "donations", force: :cascade do |t|
|
||||||
t.integer "user_id"
|
t.integer "user_id"
|
||||||
t.integer "amount_sats"
|
t.integer "amount_sats"
|
||||||
@ -48,6 +48,7 @@ ActiveRecord::Schema[7.0].define(version: 2021_11_20_010540) do
|
|||||||
t.string "unconfirmed_email"
|
t.string "unconfirmed_email"
|
||||||
t.text "ln_login_ciphertext"
|
t.text "ln_login_ciphertext"
|
||||||
t.text "ln_password_ciphertext"
|
t.text "ln_password_ciphertext"
|
||||||
|
t.string "ln_account"
|
||||||
t.index ["email"], name: "index_users_on_email", unique: true
|
t.index ["email"], name: "index_users_on_email", unique: true
|
||||||
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
|
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
|
||||||
end
|
end
|
||||||
|
@ -2,8 +2,10 @@ FactoryBot.define do
|
|||||||
factory :user do
|
factory :user do
|
||||||
id { 1 }
|
id { 1 }
|
||||||
cn { "jimmy" }
|
cn { "jimmy" }
|
||||||
|
ou { "kosmos.org" }
|
||||||
email { "jimmy@example.com" }
|
email { "jimmy@example.com" }
|
||||||
password { "dis-muh-password" }
|
password { "dis-muh-password" }
|
||||||
confirmed_at { DateTime.now }
|
confirmed_at { DateTime.now }
|
||||||
|
ln_account { "123456" }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
19
spec/fixtures/lndhub/incoming.json
vendored
Normal file
19
spec/fixtures/lndhub/incoming.json
vendored
Normal 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
19
spec/fixtures/lndhub/outgoing.json
vendored
Normal 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"
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
require 'webmock/rspec'
|
require 'webmock/rspec'
|
||||||
|
|
||||||
RSpec.describe ExchangeXmppContactsJob, type: :job do
|
RSpec.describe XmppExchangeContactsJob, type: :job do
|
||||||
let(:user) { create :user, cn: "willherschel", ou: "kosmos.org" }
|
let(:user) { create :user, cn: "willherschel", ou: "kosmos.org" }
|
||||||
|
|
||||||
subject(:job) {
|
subject(:job) {
|
77
spec/requests/webhooks_spec.rb
Normal file
77
spec/requests/webhooks_spec.rb
Normal 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
|
Loading…
x
Reference in New Issue
Block a user