lighstorm 0.0.7 → 0.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/.env.example +1 -0
  3. data/.gitignore +1 -0
  4. data/.rubocop.yml +1 -0
  5. data/Gemfile +5 -3
  6. data/Gemfile.lock +15 -10
  7. data/README.md +4 -3
  8. data/Rakefile +11 -0
  9. data/adapters/edges/payment/purpose.rb +22 -11
  10. data/adapters/edges/payment.rb +51 -12
  11. data/adapters/invoice.rb +139 -17
  12. data/components/cache.rb +7 -2
  13. data/controllers/forward/group_by_channel.rb +1 -1
  14. data/controllers/invoice/actions/create.rb +22 -6
  15. data/controllers/invoice/actions/pay.rb +71 -13
  16. data/controllers/invoice/all.rb +16 -6
  17. data/controllers/invoice/decode.rb +44 -0
  18. data/controllers/invoice/find_by_secret_hash.rb +7 -1
  19. data/controllers/invoice.rb +17 -3
  20. data/controllers/node/actions/pay.rb +109 -0
  21. data/controllers/payment/actions/pay.rb +104 -0
  22. data/controllers/payment/all.rb +49 -16
  23. data/controllers/transaction/all.rb +54 -0
  24. data/controllers/transaction.rb +13 -0
  25. data/deleted.sh +1 -0
  26. data/docs/README.md +292 -49
  27. data/docs/_coverpage.md +1 -1
  28. data/docs/index.html +1 -1
  29. data/helpers/time_expression.rb +33 -0
  30. data/models/connections/payment_channel.rb +13 -8
  31. data/models/edges/channel.rb +1 -1
  32. data/models/edges/payment.rb +51 -20
  33. data/models/errors.rb +29 -1
  34. data/models/invoice.rb +67 -11
  35. data/models/nodes/node.rb +32 -0
  36. data/models/satoshis.rb +11 -3
  37. data/models/secret.rb +31 -0
  38. data/models/transaction.rb +42 -0
  39. data/ports/dsl/lighstorm.rb +2 -0
  40. data/static/cache.rb +14 -13
  41. data/static/spec.rb +1 -1
  42. metadata +12 -4
  43. data/adapters/payment_request.rb +0 -87
  44. data/models/payment_request.rb +0 -72
@@ -8,7 +8,9 @@ module Lighstorm
8
8
  module Controllers
9
9
  module Invoice
10
10
  module All
11
- def self.fetch(limit: nil)
11
+ def self.fetch(limit: nil, spontaneous: false)
12
+ at = Time.now
13
+
12
14
  last_offset = 0
13
15
 
14
16
  invoices = []
@@ -19,7 +21,9 @@ module Lighstorm
19
21
  num_max_invoices: 10
20
22
  )
21
23
 
22
- response.invoices.each { |invoice| invoices << invoice.to_h }
24
+ response.invoices.each do |invoice|
25
+ invoices << invoice.to_h if spontaneous || !invoice.payment_request.empty?
26
+ end
23
27
 
24
28
  # TODO: How to optimize this?
25
29
  # break if !limit.nil? && invoices.size >= limit
@@ -33,13 +37,15 @@ module Lighstorm
33
37
 
34
38
  invoices = invoices[0..limit - 1] unless limit.nil?
35
39
 
36
- { list_invoices: invoices }
40
+ { at: at, list_invoices: invoices }
37
41
  end
38
42
 
39
43
  def self.adapt(raw)
44
+ raise 'missing at' if raw[:at].nil?
45
+
40
46
  {
41
47
  list_invoices: raw[:list_invoices].map do |raw_invoice|
42
- Lighstorm::Adapter::Invoice.list_invoices(raw_invoice)
48
+ Lighstorm::Adapter::Invoice.list_invoices(raw_invoice, raw[:at])
43
49
  end
44
50
  }
45
51
  end
@@ -51,8 +57,12 @@ module Lighstorm
51
57
  end
52
58
  end
53
59
 
54
- def self.data(limit: nil, &vcr)
55
- raw = vcr.nil? ? fetch(limit: limit) : vcr.call(-> { fetch(limit: limit) })
60
+ def self.data(limit: nil, spontaneous: false, &vcr)
61
+ raw = if vcr.nil?
62
+ fetch(limit: limit, spontaneous: spontaneous)
63
+ else
64
+ vcr.call(-> { fetch(limit: limit, spontaneous: spontaneous) })
65
+ end
56
66
 
57
67
  adapted = adapt(raw)
58
68
 
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../ports/grpc'
4
+ require_relative '../../adapters/invoice'
5
+ require_relative '../../models/invoice'
6
+
7
+ module Lighstorm
8
+ module Controllers
9
+ module Invoice
10
+ module Decode
11
+ def self.fetch(request_code)
12
+ {
13
+ _request_code: request_code,
14
+ decode_pay_req: Ports::GRPC.lightning.decode_pay_req(pay_req: request_code).to_h
15
+ }
16
+ end
17
+
18
+ def self.adapt(raw)
19
+ {
20
+ decode_pay_req: Lighstorm::Adapter::Invoice.decode_pay_req(
21
+ raw[:decode_pay_req], raw[:_request_code]
22
+ )
23
+ }
24
+ end
25
+
26
+ def self.transform(adapted)
27
+ adapted[:decode_pay_req]
28
+ end
29
+
30
+ def self.data(request_code, &vcr)
31
+ raw = vcr.nil? ? fetch(request_code) : vcr.call(-> { fetch(request_code) })
32
+
33
+ adapted = adapt(raw)
34
+
35
+ transform(adapted)
36
+ end
37
+
38
+ def self.model(data)
39
+ Lighstorm::Models::Invoice.new(data)
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -10,13 +10,19 @@ module Lighstorm
10
10
  module FindBySecretHash
11
11
  def self.fetch(secret_hash)
12
12
  {
13
+ at: Time.now,
13
14
  lookup_invoice: Ports::GRPC.lightning.lookup_invoice(r_hash_str: secret_hash).to_h
14
15
  }
15
16
  end
16
17
 
17
18
  def self.adapt(raw)
19
+ raise 'missing at' if raw[:at].nil?
20
+
18
21
  {
19
- lookup_invoice: Lighstorm::Adapter::Invoice.lookup_invoice(raw[:lookup_invoice])
22
+ lookup_invoice: Lighstorm::Adapter::Invoice.lookup_invoice(
23
+ raw[:lookup_invoice],
24
+ raw[:at]
25
+ )
20
26
  }
21
27
  end
22
28
 
@@ -1,14 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative './invoice/all'
4
+ require_relative './invoice/decode'
4
5
  require_relative './invoice/find_by_secret_hash'
5
6
  require_relative './invoice/actions/create'
6
7
 
7
8
  module Lighstorm
8
9
  module Controllers
9
10
  module Invoice
10
- def self.all(limit: nil)
11
- All.model(All.data(limit: limit))
11
+ def self.all(limit: nil, spontaneous: false)
12
+ All.model(All.data(limit: limit, spontaneous: spontaneous))
12
13
  end
13
14
 
14
15
  def self.first
@@ -23,10 +24,23 @@ module Lighstorm
23
24
  FindBySecretHash.model(FindBySecretHash.data(secret_hash))
24
25
  end
25
26
 
26
- def self.create(description: nil, millisatoshis: nil, preview: false, &vcr)
27
+ def self.decode(request_code, &vcr)
28
+ Decode.model(Decode.data(request_code, &vcr))
29
+ end
30
+
31
+ def self.create(
32
+ payable:,
33
+ description: nil, millisatoshis: nil,
34
+ # Lightning Invoice Expiration: UX Considerations
35
+ # https://d.elor.me/2022/01/lightning-invoice-expiration-ux-considerations/
36
+ expires_in: { hours: 24 },
37
+ preview: false, &vcr
38
+ )
27
39
  Create.perform(
40
+ payable: payable,
28
41
  description: description,
29
42
  millisatoshis: millisatoshis,
43
+ expires_in: expires_in,
30
44
  preview: preview,
31
45
  &vcr
32
46
  )
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+ require 'digest'
5
+
6
+ require_relative '../../../ports/grpc'
7
+ require_relative '../../../models/secret'
8
+ require_relative '../../../models/errors'
9
+ require_relative '../../../models/edges/payment'
10
+ require_relative '../../../adapters/edges/payment'
11
+ require_relative '../../invoice'
12
+ require_relative '../../action'
13
+ require_relative '../../node/myself'
14
+
15
+ require_relative '../../payment/actions/pay'
16
+
17
+ module Lighstorm
18
+ module Controllers
19
+ module Node
20
+ module Pay
21
+ def self.dispatch(grpc_request, &vcr)
22
+ Payment::Pay.dispatch(grpc_request, &vcr)
23
+ end
24
+
25
+ def self.fetch(&vcr)
26
+ Payment::Pay.fetch(&vcr)
27
+ end
28
+
29
+ def self.adapt(data, node_get_info)
30
+ Payment::Pay.adapt(data, node_get_info)
31
+ end
32
+
33
+ def self.model(data)
34
+ Payment::Pay.model(data)
35
+ end
36
+
37
+ def self.prepare(public_key:, millisatoshis:, times_out_in:, secret:, through:, message: nil)
38
+ # Appreciation note for people that suffered in the past and shared
39
+ # their knowledge, so we don't have to struggle the same:
40
+ # - https://github.com/lightningnetwork/lnd/discussions/6357
41
+ # - https://docs.lightning.engineering/lightning-network-tools/lnd/send-messages-with-keysend
42
+ # - https://peakd.com/@brianoflondon/lightning-keysend-is-strange-and-how-to-send-keysend-payment-in-lightning-with-the-lnd-rest-api-via-python
43
+ # We are standing on the shoulders of giants, thank you very much. :)
44
+ request = {
45
+ service: :router,
46
+ method: :send_payment_v2,
47
+ params: {
48
+ dest: [public_key].pack('H*'),
49
+ amt_msat: millisatoshis,
50
+ timeout_seconds: Helpers::TimeExpression.seconds(times_out_in),
51
+ allow_self_payment: true,
52
+ dest_custom_records: {}
53
+ }
54
+ }
55
+
56
+ if !message.nil? && !message.empty?
57
+ # https://github.com/satoshisstream/satoshis.stream/blob/main/TLV_registry.md
58
+ request[:params][:dest_custom_records][34_349_334] = message
59
+ end
60
+
61
+ if through.to_sym == :keysend
62
+ request[:params][:payment_hash] = [secret[:hash]].pack('H*')
63
+ request[:params][:dest_custom_records][5_482_373_484] = [secret[:preimage]].pack('H*')
64
+ elsif through.to_sym == :amp
65
+ request[:params][:amp] = true
66
+ end
67
+
68
+ request[:params].delete(:dest_custom_records) if request[:params][:dest_custom_records].empty?
69
+
70
+ request
71
+ end
72
+
73
+ def self.perform(
74
+ public_key:, millisatoshis:, through:,
75
+ times_out_in:,
76
+ message: nil, secret: nil,
77
+ preview: false, &vcr
78
+ )
79
+ secret = Models::Secret.create.to_h if secret.nil? && through.to_sym == :keysend
80
+
81
+ grpc_request = prepare(
82
+ public_key: public_key,
83
+ millisatoshis: millisatoshis,
84
+ through: through,
85
+ times_out_in: times_out_in,
86
+ secret: secret,
87
+ message: message
88
+ )
89
+
90
+ return grpc_request if preview
91
+
92
+ response = dispatch(grpc_request, &vcr)
93
+
94
+ Payment::Pay.raise_error_if_exists!(response)
95
+
96
+ data = fetch(&vcr)
97
+
98
+ adapted = adapt(response, data)
99
+
100
+ model = self.model(adapted)
101
+
102
+ Payment::Pay.raise_failure_if_exists!(model, response)
103
+
104
+ Action::Output.new({ response: response[:response], result: model })
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+ require 'digest'
5
+
6
+ require_relative '../../../ports/grpc'
7
+ require_relative '../../../models/errors'
8
+ require_relative '../../../models/edges/payment'
9
+ require_relative '../../../adapters/edges/payment'
10
+ require_relative '../../node/myself'
11
+ require_relative '../../invoice/decode'
12
+
13
+ module Lighstorm
14
+ module Controllers
15
+ module Payment
16
+ module Pay
17
+ def self.call(grpc_request)
18
+ result = []
19
+ Lighstorm::Ports::GRPC.send(grpc_request[:service]).send(
20
+ grpc_request[:method], grpc_request[:params]
21
+ ) do |response|
22
+ result << response.to_h
23
+ end
24
+ { response: result, exception: nil }
25
+ rescue StandardError => e
26
+ { exception: e }
27
+ end
28
+
29
+ def self.dispatch(grpc_request, &vcr)
30
+ vcr.nil? ? call(grpc_request) : vcr.call(-> { call(grpc_request) }, :dispatch)
31
+ end
32
+
33
+ def self.fetch_all(request_code)
34
+ {
35
+ invoice_decode: request_code.nil? ? nil : Invoice::Decode.data(request_code),
36
+ node_myself: Node::Myself.data
37
+ }
38
+ end
39
+
40
+ def self.fetch(request_code = nil, &vcr)
41
+ raw = vcr.nil? ? fetch_all(request_code) : vcr.call(-> { fetch_all(request_code) })
42
+ end
43
+
44
+ def self.adapt(grpc_data, fetch_data)
45
+ Adapter::Payment.send_payment_v2(
46
+ grpc_data[:response].last,
47
+ fetch_data[:node_myself],
48
+ fetch_data[:invoice_decode]
49
+ )
50
+ end
51
+
52
+ def self.model(data)
53
+ Models::Payment.new(data)
54
+ end
55
+
56
+ def self.raise_error_if_exists!(response)
57
+ return if response[:exception].nil?
58
+
59
+ if response[:exception].is_a?(GRPC::AlreadyExists)
60
+ raise AlreadyPaidError.new(
61
+ 'The invoice is already paid.',
62
+ grpc: response[:exception]
63
+ )
64
+ end
65
+
66
+ if response[:exception].message =~ /amount must not be specified when paying a non-zero/
67
+ raise AmountForNonZeroError.new(
68
+ 'Millisatoshis must not be specified when paying a non-zero amount invoice.',
69
+ grpc: response[:exception]
70
+ )
71
+ end
72
+
73
+ if response[:exception].message =~ /amount must be specified when paying a zero amount/
74
+ raise MissingMillisatoshisError.new(
75
+ 'Millisatoshis must be specified when paying a zero amount invoice.',
76
+ grpc: response[:exception]
77
+ )
78
+ end
79
+
80
+ raise PaymentError.new(
81
+ response[:exception].message,
82
+ grpc: response[:exception]
83
+ )
84
+ end
85
+
86
+ def self.raise_failure_if_exists!(model, response)
87
+ return unless model.state == 'failed'
88
+
89
+ if response[:response].last[:failure_reason] == :FAILURE_REASON_NO_ROUTE
90
+ raise NoRouteFoundError.new(
91
+ response[:response].last[:failure_reason],
92
+ response: response[:response], result: model
93
+ )
94
+ else
95
+ raise PaymentError.new(
96
+ response[:response].last[:failure_reason],
97
+ response: response[:response], result: model
98
+ )
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
@@ -195,7 +195,7 @@ module Lighstorm
195
195
  raw[:decode_pay_req].each_key do |key|
196
196
  next if raw[:decode_pay_req][key][:_error]
197
197
 
198
- adapted[:decode_pay_req][key] = Lighstorm::Adapter::PaymentRequest.decode_pay_req(
198
+ adapted[:decode_pay_req][key] = Lighstorm::Adapter::Invoice.decode_pay_req(
199
199
  raw[:decode_pay_req][key]
200
200
  )
201
201
  end
@@ -204,7 +204,7 @@ module Lighstorm
204
204
  next if raw[:lookup_invoice][key][:_error]
205
205
 
206
206
  adapted[:lookup_invoice][key] = Lighstorm::Adapter::Invoice.lookup_invoice(
207
- raw[:lookup_invoice][key]
207
+ raw[:lookup_invoice][key], raw[:at]
208
208
  )
209
209
  end
210
210
 
@@ -309,34 +309,67 @@ module Lighstorm
309
309
  end
310
310
 
311
311
  def self.transform(list_payments, adapted)
312
- if adapted[:lookup_invoice][list_payments[:request][:secret][:hash]] &&
313
- !adapted[:lookup_invoice][list_payments[:request][:secret][:hash]][:_error]
312
+ if adapted[:lookup_invoice][list_payments[:secret][:hash]] &&
313
+ !adapted[:lookup_invoice][list_payments[:secret][:hash]][:_error]
314
314
 
315
- list_payments[:request] = adapted[:lookup_invoice][list_payments[:request][:secret][:hash]][:request]
316
- else
317
- list_payments[:request][:_key] = Digest::SHA256.hexdigest(
318
- list_payments[:request][:code]
319
- )
315
+ if list_payments[:invoice]
316
+ lookup = adapted[:lookup_invoice][list_payments[:secret][:hash]]
317
+
318
+ list_payments[:invoice][:description] = lookup[:description]
319
+
320
+ lookup.each_key do |key|
321
+ if lookup[key].is_a?(Hash)
322
+ unless list_payments[:invoice].key?(key) && !list_payments[:invoice][key].nil?
323
+ list_payments[:invoice][key] = lookup[:key]
324
+ end
325
+
326
+ next
327
+ end
328
+
329
+ unless list_payments[:invoice].key?(key) && !list_payments[:invoice][key].nil? &&
330
+ (!list_payments[:invoice][key].is_a?(String) || !list_payments[:invoice][key].empty?)
331
+ list_payments[:invoice][key] = lookup[key]
332
+ end
333
+ end
334
+ else
335
+ list_payments[:invoice] = adapted[:lookup_invoice][list_payments[:secret][:hash]]
336
+ end
320
337
  end
338
+
321
339
  list_payments[:hops].each do |hop|
322
340
  hop[:channel] = transform_channel(hop[:channel], adapted)
323
341
  end
324
342
 
325
- if adapted[:decode_pay_req][list_payments[:request][:code]]
326
- decoded = adapted[:decode_pay_req][list_payments[:request][:code]]
327
- request = list_payments[:request]
343
+ if adapted[:decode_pay_req][list_payments[:invoice][:code]]
344
+ decoded = adapted[:decode_pay_req][list_payments[:invoice][:code]]
345
+ invoice = list_payments[:invoice]
328
346
 
329
347
  decoded.each_key do |key|
330
- request[key] = decoded[key] unless request.key?(key)
348
+ if !decoded[key].is_a?(Hash)
349
+ invoice[key] = decoded[key]
350
+ elsif decoded[key].is_a?(Hash)
351
+ invoice[key] = {} unless invoice.key?(key)
352
+
353
+ next if key == :secret
331
354
 
332
- next unless decoded[key].is_a?(Hash)
355
+ decoded[key].each_key do |sub_key|
356
+ next if decoded[key][sub_key].nil? ||
357
+ (decoded[key][sub_key].is_a?(String) && decoded[key][sub_key].empty?)
333
358
 
334
- decoded[key].each_key do |sub_key|
335
- request[key][sub_key] = decoded[key][sub_key] unless request[key].key?(sub_key)
359
+ invoice[key][sub_key] = decoded[key][sub_key]
360
+ end
336
361
  end
337
362
  end
338
363
  end
339
364
 
365
+ if list_payments[:invoice][:code]
366
+ if list_payments[:invoice][:payable] == 'once'
367
+ list_payments[:through] = 'non-amp'
368
+ elsif list_payments[:invoice][:payable] == 'indefinitely'
369
+ list_payments[:through] = 'amp'
370
+ end
371
+ end
372
+
340
373
  list_payments
341
374
  end
342
375
 
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../invoice/all'
4
+ require_relative '../../models/transaction'
5
+
6
+ module Lighstorm
7
+ module Controllers
8
+ module Transaction
9
+ module All
10
+ def self.fetch(limit: nil)
11
+ transactions = []
12
+
13
+ Invoice::All.data(spontaneous: true).filter do |invoice|
14
+ !invoice[:payments].nil? && invoice[:payments].size.positive?
15
+ end.each do |invoice|
16
+ invoice[:payments].each do |payment|
17
+ transactions << {
18
+ direction: 'in',
19
+ at: payment[:at],
20
+ amount: payment[:amount],
21
+ message: payment[:message],
22
+ kind: 'invoice',
23
+ data: invoice
24
+ }
25
+ end
26
+ end
27
+
28
+ transactions = transactions.sort_by { |transaction| -transaction[:at].to_i }
29
+
30
+ transactions = transactions[0..limit - 1] unless limit.nil?
31
+
32
+ { transactions: transactions }
33
+ end
34
+
35
+ def self.transform(raw)
36
+ raw[:transactions].map do |transaction|
37
+ transaction[:_key] = SecureRandom.hex
38
+ transaction
39
+ end
40
+ end
41
+
42
+ def self.data(limit: nil, &vcr)
43
+ raw = vcr.nil? ? fetch(limit: limit) : vcr.call(-> { fetch(limit: limit) })
44
+
45
+ transform(raw)
46
+ end
47
+
48
+ def self.model(data)
49
+ data.map { |data| Models::Transaction.new(data) }
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './transaction/all'
4
+
5
+ module Lighstorm
6
+ module Controllers
7
+ module Transaction
8
+ def self.all(limit: nil)
9
+ All.model(All.data(limit: limit))
10
+ end
11
+ end
12
+ end
13
+ end
data/deleted.sh ADDED
@@ -0,0 +1 @@
1
+ deleted.sh