Class: Lnurl

Inherits:
Object
  • Object
show all
Defined in:
lib/lnurl.rb

Constant Summary collapse

VERSION =
'1.1.1'.freeze
MAX_INTEGER =

Maximum integer size Useful for max_length when decoding

2**31 - 1
InvoiceResponse =
Class.new(OpenStruct)
LnurlResponse =
Class.new(OpenStruct) do
  # amount in msats
  def request_invoice(args)
    args.transform_keys!(&:to_s)
    callback_uri = URI(callback)
    if callback_uri.query
      args = Hash[URI.decode_www_form(callback_uri.query)].merge(args) # reverse merge
    end
    callback_uri.query = URI.encode_www_form(args)
    body = Lnurl.http_get(callback_uri)
    InvoiceResponse.new JSON.parse(body)
  end
end
HRP =
'lnurl'.freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(uri) ⇒ Lnurl

Returns a new instance of Lnurl.



32
33
34
# File 'lib/lnurl.rb', line 32

def initialize(uri)
  @uri = URI(uri)
end

Instance Attribute Details

#uriObject (readonly)

Returns the value of attribute uri.



30
31
32
# File 'lib/lnurl.rb', line 30

def uri
  @uri
end

Class Method Details

.convert_bits(data, from, to, padding = true) ⇒ Object



89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/lnurl.rb', line 89

def self.convert_bits(data, from, to, padding=true)
  acc = 0
  bits = 0
  ret = []
  maxv = (1 << to) - 1
  max_acc = (1 << (from + to - 1)) - 1
  data.each do |v|
    return nil if v < 0 || (v >> from) != 0
    acc = ((acc << from) | v) & max_acc
    bits += from
    while bits >= to
      bits -= to
      ret << ((acc >> bits) & maxv)
    end
  end
  if padding
    ret << ((acc << (to - bits)) & maxv) unless bits == 0
  elsif bits >= from || ((acc << (to - bits)) & maxv) != 0
    return nil
  end
  ret
end

.decode(lnurl, max_length = MAX_INTEGER) ⇒ Object



68
69
70
# File 'lib/lnurl.rb', line 68

def self.decode(lnurl, max_length = MAX_INTEGER)
  Lnurl.new(decode_raw(lnurl, max_length))
end

.decode_lightning_address(lightning_address) ⇒ Object



83
84
85
86
# File 'lib/lnurl.rb', line 83

def self.decode_lightning_address(lightning_address)
  username, domain = lightning_address.split('@')
  "https://#{domain}/.well-known/lnurlp/#{username}"
end

.decode_raw(lnurl, max_length = MAX_INTEGER) ⇒ Object



72
73
74
75
76
77
# File 'lib/lnurl.rb', line 72

def self.decode_raw(lnurl, max_length = MAX_INTEGER)
  lnurl = lnurl.gsub(/^lightning:/, '')
  hrp, data, sepc = Bech32.decode(lnurl, max_length)
  # raise 'no lnurl' if hrp != HRP
  convert_bits(data, 5, 8, false).pack('C*').force_encoding('utf-8')
end

.from_lightning_address(lightning_address) ⇒ Object



79
80
81
# File 'lib/lnurl.rb', line 79

def self.from_lightning_address(lightning_address)
  Lnurl.new(decode_lightning_address(lightning_address))
end

.http_get(uri, limit = 10) ⇒ Object

Handles HTTP GET requests and follows redirects if necessary

Raises:

  • (ArgumentError)


113
114
115
116
117
118
119
120
121
122
123
124
125
# File 'lib/lnurl.rb', line 113

def self.http_get(uri, limit = 10)
  raise ArgumentError, 'too many HTTP redirects' if limit.zero?

  response = Net::HTTP.get_response(uri)

  case response
  when Net::HTTPRedirection
    location = response['location']
    http_get(URI(location), limit - 1)
  else
    response.body
  end
end

.valid?(value) ⇒ Boolean

Returns:

  • (Boolean)


60
61
62
63
64
65
66
# File 'lib/lnurl.rb', line 60

def self.valid?(value)
  return false unless value.to_s.downcase.match?(Regexp.new("^#{HRP}", 'i')) # false if the HRP does not match
  decoded = decode_raw(value) rescue false # rescue any decoding errors
  return false unless decoded # false if it could not get decoded

  return decoded.match?(URI.regexp) # check if the URI is valid
end

Instance Method Details

#dataObject



41
42
43
# File 'lib/lnurl.rb', line 41

def data
  self.class.convert_bits(uri.to_s.codepoints, 8, 5, true)
end

#payment_request(amount:) ⇒ Object



56
57
58
# File 'lib/lnurl.rb', line 56

def payment_request(amount:)
  request_invoice(amount: amount).pr
end

#request_invoice(amount:) ⇒ Object



52
53
54
# File 'lib/lnurl.rb', line 52

def request_invoice(amount:)
  response.request_invoice(amount: amount)
end

#responseObject



45
46
47
48
49
50
# File 'lib/lnurl.rb', line 45

def response
  @response ||= begin
                  body = self.class.http_get(uri)
                  LnurlResponse.new JSON.parse(body)
                end
end

#to_bech32Object Also known as: encode



36
37
38
# File 'lib/lnurl.rb', line 36

def to_bech32
  Bech32.encode(HRP, data, Bech32::Encoding::BECH32).upcase
end