mirror of
https://github.com/bumi/faraday_ln_paywall
synced 2025-08-02 02:14:12 +00:00
82 lines
2.7 KiB
Ruby
82 lines
2.7 KiB
Ruby
require 'grpc'
|
|
require 'lightning/invoice'
|
|
|
|
ENV['GRPC_SSL_CIPHER_SUITES'] = "HIGH+ECDSA"
|
|
|
|
module FaradayLnPaywall
|
|
class PaymentError < StandardError; end
|
|
|
|
class Middleware < Faraday::Middleware
|
|
|
|
def initialize(app, options = {})
|
|
super(app)
|
|
@options = options
|
|
@options[:address] ||= 'localhost:10009'
|
|
@options[:timeout] ||= 5
|
|
@options[:credentials] ||= File.read(File.expand_path(@options[:credentials_path] || "~/.lnd/tls.cert"))
|
|
@options[:macaroon] ||= begin
|
|
macaroon_binary = ::File.read(::File.expand_path(@options[:macaroon_path] || "~/.lnd/data/chain/bitcoin/testnet/admin.macaroon"))
|
|
macaroon_binary.each_byte.map { |b| b.to_s(16).rjust(2,'0') }.join
|
|
end
|
|
@lnd_client = Lnrpc::Lightning::Stub.new(@options[:address], GRPC::Core::ChannelCredentials.new(@options[:credentials]))
|
|
end
|
|
|
|
def payment_requested?(env)
|
|
env[:status] == 402 && env[:response_headers]['Content-Type'] == "application/vnd.lightning.bolt11"
|
|
end
|
|
|
|
def validate_invoice!(invoice)
|
|
if !@options[:max_amount].nil? && @options[:max_amount] < invoice_amount_in_satoshi(invoice)
|
|
raise PaymentError, "invoice amount greater than expected maximum of #{@options[:max_amount]}"
|
|
end
|
|
if !invoice.expiry.nil? && Time.now.to_i > invoice.timestamp + invoice.expiry.to_i
|
|
raise PaymentError, "invoice expired"
|
|
end
|
|
end
|
|
|
|
def pay(env)
|
|
invoice = Lightning::Invoice.parse(env.body)
|
|
validate_invoice!(invoice)
|
|
|
|
Timeout::timeout(@options[:timeout], PaymentError, "payment execution expired") do
|
|
@lnd_client.send_payment_sync(
|
|
Lnrpc::SendRequest.new(payment_request: env.body),
|
|
{ metadata: { macaroon: @options[:macaroon] }}
|
|
)
|
|
end
|
|
end
|
|
|
|
def call(request_env)
|
|
original_call = request_env.dup
|
|
response = @app.call(request_env)
|
|
response.on_complete do |response_env|
|
|
if payment_requested?(response_env)
|
|
payment = pay(response_env)
|
|
if payment && payment.payment_error == ""
|
|
preimage = payment.payment_preimage.each_byte.map { |b| b.to_s(16).rjust(2, '0') }.join
|
|
original_call[:request_headers].merge!('X-Preimage' => preimage)
|
|
response = @app.call(original_call)
|
|
else
|
|
raise PaymentError, payment.payment_error
|
|
end
|
|
end
|
|
end
|
|
response
|
|
end
|
|
|
|
# todo move to Lightning/invoice gem
|
|
def invoice_amount_in_satoshi(invoice)
|
|
return if invoice.amount.nil?
|
|
multi = {
|
|
'm' => 0.001,
|
|
'u' => 0.000001,
|
|
'n' => 0.000000001,
|
|
'p' => 0.000000000001
|
|
}[invoice.multiplier]
|
|
|
|
(invoice.amount * multi * 100000000).to_i # amount in bitcoin * 100000000
|
|
end
|
|
end
|
|
end
|
|
|