lighstorm 0.0.7 → 0.0.9

Sign up to get free protection for your applications and to get access to all the features.
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