├── .gitignore ├── .rspec ├── .travis.yml ├── Gemfile ├── Gemfile.lock ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin ├── console └── setup ├── lib └── lnurl.rb ├── lnurl.gemspec └── spec ├── lnurl_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | 3 | /.bundle/ 4 | /.yardoc 5 | /_yardoc/ 6 | /coverage/ 7 | /doc/ 8 | /pkg/ 9 | /spec/reports/ 10 | /tmp/ 11 | 12 | # rspec failure tracking 13 | .rspec_status 14 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | --require spec_helper 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: ruby 3 | cache: bundler 4 | rvm: 5 | - 2.7.1 6 | before_install: gem install bundler -v 2.1.4 7 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | # Specify your gem's dependencies in lnurl.gemspec 4 | gemspec 5 | 6 | gem "rake", "~> 12.0" 7 | gem "rspec", "~> 3.0" 8 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | lnurl (1.1.1) 5 | bech32 (~> 1.1) 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | bech32 (1.4.2) 11 | thor (>= 1.1.0) 12 | diff-lcs (1.4.4) 13 | rake (12.3.3) 14 | rspec (3.10.0) 15 | rspec-core (~> 3.10.0) 16 | rspec-expectations (~> 3.10.0) 17 | rspec-mocks (~> 3.10.0) 18 | rspec-core (3.10.1) 19 | rspec-support (~> 3.10.0) 20 | rspec-expectations (3.10.1) 21 | diff-lcs (>= 1.2.0, < 2.0) 22 | rspec-support (~> 3.10.0) 23 | rspec-mocks (3.10.2) 24 | diff-lcs (>= 1.2.0, < 2.0) 25 | rspec-support (~> 3.10.0) 26 | rspec-support (3.10.2) 27 | thor (1.2.2) 28 | 29 | PLATFORMS 30 | ruby 31 | 32 | DEPENDENCIES 33 | lnurl! 34 | rake (~> 12.0) 35 | rspec (~> 3.0) 36 | 37 | BUNDLED WITH 38 | 2.4.4 39 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Michael Bumann 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LNURL tools for Ruby 2 | 3 | LNURL is a protocol for interaction between Lightning wallets and third-party services. 4 | 5 | This gem provides helpers to work with LNURLs from Ruby. 6 | 7 | 8 | ## Links: 9 | 10 | * [LNURL: Lightning Network UX protocol RFC](https://github.com/btcontract/lnurl-rfc) 11 | * [Awesome LNURL - a curated list with things related to LNURL](https://github.com/fiatjaf/awesome-lnurl) 12 | * [LNURL pay flow](https://xn--57h.bigsun.xyz/lnurl-pay-flow.txt) 13 | 14 | 15 | ## Installation 16 | 17 | Add this line to your application's Gemfile: 18 | 19 | ```ruby 20 | gem 'lnurl' 21 | ``` 22 | 23 | Or install it yourself as: 24 | 25 | $ gem install lnurl 26 | 27 | ## Usage 28 | 29 | ### Encoding 30 | 31 | ```ruby 32 | lnurl = Lnurl.new('https://lnurl.com/pay') 33 | puts lnurl.to_bech32 # => LNURL1DP68GURN8GHJ7MRWW4EXCTNRDAKJ7URP0YVM59LW 34 | ``` 35 | 36 | ### Decoding 37 | 38 | ```ruby 39 | Lnurl.valid?('nolnurl') #=> false 40 | 41 | lnurl = Lnurl.decode('LNURL1DP68GURN8GHJ7MRWW4EXCTNRDAKJ7URP0YVM59LW') 42 | lnurl.uri # => # 43 | ``` 44 | 45 | By default we accept long LNURLs but you can configure a custom max length: 46 | ```ruby 47 | lnurl = Lnurl.decode(a_short_lnurl, 90) 48 | ``` 49 | 50 | ### [Lightning Address](https://github.com/andrerfneves/lightning-address) 51 | 52 | ```ruby 53 | lnurl = Lnurl.from_lightning_address('user@lnurl.com') 54 | lnurl.uri # => # 55 | ``` 56 | 57 | ### LNURL responses 58 | 59 | ```ruby 60 | lnurl = Lnurl.decode('LNURL1DP68GURN8GHJ7MRWW4EXCTNRDAKJ7URP0YVM59LW') 61 | response = lnurl.response # => # OK / ERROR 63 | response.callback # => https://... 64 | response.tag # => payRequest 65 | response.maxSendable # => 100000000 66 | response.minSendable # => 1000 67 | response.metadata # => [...] 68 | 69 | invoice = response.request_invoice(amount: 100000) # (amount in msats) # OK / ERROR 74 | invoice.pr # => lntb20u1p0tdr7mpp... 75 | invoice.successAction # => {...} 76 | invoice.routes # => [...] 77 | 78 | ``` 79 | 80 | 81 | ## Contributing 82 | 83 | Bug reports and pull requests are welcome on GitHub at https://github.com/bumi/lnurl-ruby. 84 | 85 | 86 | ## License 87 | 88 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 89 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task :default => :spec 7 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "lnurl" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start(__FILE__) 15 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /lib/lnurl.rb: -------------------------------------------------------------------------------- 1 | require 'bech32' 2 | require 'net/http' 3 | require 'json' 4 | require 'ostruct' 5 | 6 | class Lnurl 7 | VERSION = '1.1.1'.freeze 8 | 9 | # Maximum integer size 10 | # Useful for max_length when decoding 11 | MAX_INTEGER = 2**31 - 1 12 | 13 | InvoiceResponse = Class.new(OpenStruct) 14 | LnurlResponse = Class.new(OpenStruct) do 15 | # amount in msats 16 | def request_invoice(args) 17 | args.transform_keys!(&:to_s) 18 | callback_uri = URI(callback) 19 | if callback_uri.query 20 | args = Hash[URI.decode_www_form(callback_uri.query)].merge(args) # reverse merge 21 | end 22 | callback_uri.query = URI.encode_www_form(args) 23 | body = Lnurl.http_get(callback_uri) 24 | InvoiceResponse.new JSON.parse(body) 25 | end 26 | end 27 | 28 | HRP = 'lnurl'.freeze 29 | 30 | attr_reader :uri 31 | 32 | def initialize(uri) 33 | @uri = URI(uri) 34 | end 35 | 36 | def to_bech32 37 | Bech32.encode(HRP, data, Bech32::Encoding::BECH32).upcase 38 | end 39 | alias encode to_bech32 40 | 41 | def data 42 | self.class.convert_bits(uri.to_s.codepoints, 8, 5, true) 43 | end 44 | 45 | def response 46 | @response ||= begin 47 | body = self.class.http_get(uri) 48 | LnurlResponse.new JSON.parse(body) 49 | end 50 | end 51 | 52 | def request_invoice(amount:) 53 | response.request_invoice(amount: amount) 54 | end 55 | 56 | def payment_request(amount:) 57 | request_invoice(amount: amount).pr 58 | end 59 | 60 | def self.valid?(value) 61 | return false unless value.to_s.downcase.match?(Regexp.new("^#{HRP}", 'i')) # false if the HRP does not match 62 | decoded = decode_raw(value) rescue false # rescue any decoding errors 63 | return false unless decoded # false if it could not get decoded 64 | 65 | return decoded.match?(URI.regexp) # check if the URI is valid 66 | end 67 | 68 | def self.decode(lnurl, max_length = MAX_INTEGER) 69 | Lnurl.new(decode_raw(lnurl, max_length)) 70 | end 71 | 72 | def self.decode_raw(lnurl, max_length = MAX_INTEGER) 73 | lnurl = lnurl.gsub(/^lightning:/, '') 74 | hrp, data, sepc = Bech32.decode(lnurl, max_length) 75 | # raise 'no lnurl' if hrp != HRP 76 | convert_bits(data, 5, 8, false).pack('C*').force_encoding('utf-8') 77 | end 78 | 79 | def self.from_lightning_address(lightning_address) 80 | Lnurl.new(decode_lightning_address(lightning_address)) 81 | end 82 | 83 | def self.decode_lightning_address(lightning_address) 84 | username, domain = lightning_address.split('@') 85 | "https://#{domain}/.well-known/lnurlp/#{username}" 86 | end 87 | 88 | # FROM: https://github.com/azuchi/bech32rb/blob/master/lib/bech32/segwit_addr.rb 89 | def self.convert_bits(data, from, to, padding=true) 90 | acc = 0 91 | bits = 0 92 | ret = [] 93 | maxv = (1 << to) - 1 94 | max_acc = (1 << (from + to - 1)) - 1 95 | data.each do |v| 96 | return nil if v < 0 || (v >> from) != 0 97 | acc = ((acc << from) | v) & max_acc 98 | bits += from 99 | while bits >= to 100 | bits -= to 101 | ret << ((acc >> bits) & maxv) 102 | end 103 | end 104 | if padding 105 | ret << ((acc << (to - bits)) & maxv) unless bits == 0 106 | elsif bits >= from || ((acc << (to - bits)) & maxv) != 0 107 | return nil 108 | end 109 | ret 110 | end 111 | 112 | # Handles HTTP GET requests and follows redirects if necessary 113 | def self.http_get(uri, limit = 10) 114 | raise ArgumentError, 'too many HTTP redirects' if limit.zero? 115 | 116 | response = Net::HTTP.get_response(uri) 117 | 118 | case response 119 | when Net::HTTPRedirection 120 | location = response['location'] 121 | http_get(URI(location), limit - 1) 122 | else 123 | response.body 124 | end 125 | end 126 | end 127 | -------------------------------------------------------------------------------- /lnurl.gemspec: -------------------------------------------------------------------------------- 1 | require_relative 'lib/lnurl' 2 | 3 | Gem::Specification.new do |spec| 4 | spec.name = "lnurl" 5 | spec.version = Lnurl::VERSION 6 | spec.authors = ["Michael Bumann"] 7 | spec.email = ["hello@michaelbumann.com"] 8 | 9 | spec.summary = %q{LNURL implementation for ruby} 10 | spec.description = %q{A collection of tools to work with LNURLs - the protocol for interaction between Lightning wallets and third-party services.} 11 | spec.homepage = "https://github.com/bumi/lnurl-ruby" 12 | spec.license = "MIT" 13 | spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0") 14 | 15 | spec.metadata["homepage_uri"] = spec.homepage 16 | spec.metadata["source_code_uri"] = "https://github.com/bumi/lnurl-ruby" 17 | # spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here." 18 | 19 | spec.metadata['funding'] = 'lightning:02ad33d99d0bb3bf3bb8ec8e089cbefa8fd7de23a13cfa59aec9af9730816be76f' 20 | 21 | # Specify which files should be added to the gem when it is released. 22 | # The `git ls-files -z` loads the files in the RubyGem that have been added into git. 23 | spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do 24 | `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 25 | end 26 | spec.bindir = "exe" 27 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 28 | spec.require_paths = ["lib"] 29 | 30 | spec.add_runtime_dependency 'bech32', '~> 1.1' 31 | end 32 | -------------------------------------------------------------------------------- /spec/lnurl_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe Lnurl do 2 | it "has a version number" do 3 | expect(Lnurl::VERSION).not_to be nil 4 | end 5 | 6 | it "does something useful" do 7 | expect(false).to eq(true) 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require "bundler/setup" 2 | require "lnurl" 3 | 4 | RSpec.configure do |config| 5 | # Enable flags like --only-failures and --next-failure 6 | config.example_status_persistence_file_path = ".rspec_status" 7 | 8 | # Disable RSpec exposing methods globally on `Module` and `main` 9 | config.disable_monkey_patching! 10 | 11 | config.expect_with :rspec do |c| 12 | c.syntax = :expect 13 | end 14 | end 15 | --------------------------------------------------------------------------------