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
continuous-integration/drone/push Build is passing
Details
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_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_API_URL='http://10.1.1.163:3026'
|
||||
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'
|
||||
|
||||
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'
|
||||
|
|
|
@ -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
|
||||
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
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
class ExchangeXmppContactsJob < ApplicationJob
|
||||
class XmppExchangeContactsJob < ApplicationJob
|
||||
queue_as :default
|
||||
|
||||
def perform(inviter, username, domain)
|
|
@ -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
|
||||
#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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
||||
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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
}
|
|
@ -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 '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) {
|
|
@ -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…
Reference in New Issue