lighstorm 0.0.4 → 0.0.5

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/.github/workflows/ruby-rspec-tests.yml +39 -0
  3. data/Gemfile.lock +1 -1
  4. data/README.md +5 -5
  5. data/adapters/connections/channel_node/policy.rb +61 -0
  6. data/adapters/connections/channel_node.rb +40 -18
  7. data/adapters/edges/channel.rb +23 -7
  8. data/adapters/edges/forward.rb +1 -2
  9. data/adapters/edges/payment/purpose.rb +0 -2
  10. data/adapters/edges/payment.rb +2 -5
  11. data/adapters/invoice.rb +2 -3
  12. data/adapters/nodes/node.rb +39 -5
  13. data/adapters/payment_request.rb +0 -1
  14. data/controllers/channel/actions/apply_gossip.rb +194 -0
  15. data/controllers/channel/find_by_id.rb +1 -1
  16. data/controllers/channel/mine.rb +1 -1
  17. data/controllers/channel.rb +4 -0
  18. data/controllers/node/actions/apply_gossip.rb +112 -0
  19. data/controllers/node.rb +4 -0
  20. data/controllers/payment/all.rb +52 -36
  21. data/controllers/payment.rb +6 -6
  22. data/docs/README.md +89 -13
  23. data/docs/_coverpage.md +1 -1
  24. data/docs/index.html +1 -1
  25. data/models/concerns/protectable.rb +23 -0
  26. data/models/connections/channel_node/accounting.rb +4 -0
  27. data/models/connections/channel_node/fee.rb +27 -18
  28. data/models/connections/channel_node/htlc/blocks/delta.rb +39 -0
  29. data/models/connections/channel_node/htlc.rb +48 -14
  30. data/models/connections/channel_node/policy.rb +11 -2
  31. data/models/connections/channel_node.rb +33 -1
  32. data/models/edges/channel/accounting.rb +17 -0
  33. data/models/edges/channel.rb +39 -5
  34. data/models/edges/forward.rb +0 -1
  35. data/models/edges/payment.rb +0 -1
  36. data/models/errors.rb +3 -0
  37. data/models/nodes/node/lightning.rb +6 -0
  38. data/models/nodes/node/platform.rb +6 -0
  39. data/models/nodes/node.rb +51 -0
  40. data/models/payment_request.rb +6 -3
  41. data/ports/grpc/session.rb +26 -0
  42. data/ports/grpc.rb +23 -5
  43. data/static/spec.rb +1 -1
  44. metadata +9 -2
@@ -4,6 +4,7 @@ require 'securerandom'
4
4
 
5
5
  require_relative '../../satoshis'
6
6
  require_relative '../../rate'
7
+ require_relative '../../concerns/protectable'
7
8
 
8
9
  require_relative '../../../components/lnd'
9
10
  require_relative '../../../controllers/channel/actions/update_fee'
@@ -11,24 +12,38 @@ require_relative '../../../controllers/channel/actions/update_fee'
11
12
  module Lighstorm
12
13
  module Models
13
14
  class Fee
15
+ include Protectable
16
+
14
17
  def initialize(policy, data)
15
18
  @policy = policy
16
19
  @data = data
17
20
  end
18
21
 
19
22
  def base
23
+ return nil unless @data[:base]
24
+
20
25
  @base ||= Satoshis.new(milisatoshis: @data[:base][:milisatoshis])
21
26
  end
22
27
 
23
28
  def rate
29
+ return nil unless @data[:rate]
30
+
24
31
  @rate ||= Rate.new(parts_per_million: @data[:rate][:parts_per_million])
25
32
  end
26
33
 
27
34
  def to_h
28
- {
29
- base: base.to_h,
30
- rate: rate.to_h
31
- }
35
+ result = {}
36
+
37
+ result[:base] = base.to_h if @data[:base]
38
+ result[:rate] = rate.to_h if @data[:rate]
39
+
40
+ return nil if result.empty?
41
+
42
+ result
43
+ end
44
+
45
+ def dump
46
+ Marshal.load(Marshal.dump(@data))
32
47
  end
33
48
 
34
49
  def update(params, preview: false, fake: false)
@@ -38,29 +53,23 @@ module Lighstorm
38
53
  end
39
54
 
40
55
  def base=(value)
41
- validate_token!(value)
56
+ protect!(value)
42
57
 
43
58
  @base = value[:value]
44
- end
45
59
 
46
- def rate=(value)
47
- validate_token!(value)
60
+ @data[:base] = { milisatoshis: @base.milisatoshis }
48
61
 
49
- @rate = value[:value]
62
+ base
50
63
  end
51
64
 
52
- def prepare_token!(token)
53
- @token = token
54
- end
55
-
56
- private
65
+ def rate=(value)
66
+ protect!(value)
57
67
 
58
- def validate_token!(value)
59
- token = value.is_a?(Hash) ? value[:token] : nil
68
+ @rate = value[:value]
60
69
 
61
- raise OperationNotAllowedError if token.nil? || @token.nil? || token != @token
70
+ @data[:rate] = { parts_per_million: @rate.parts_per_million }
62
71
 
63
- @token = nil
72
+ rate
64
73
  end
65
74
  end
66
75
  end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../../../concerns/protectable'
4
+
5
+ module Lighstorm
6
+ module Models
7
+ class BlocksDelta
8
+ include Protectable
9
+
10
+ def initialize(data)
11
+ @data = data
12
+ end
13
+
14
+ def minimum
15
+ @minimum ||= @data[:minimum]
16
+ end
17
+
18
+ def to_h
19
+ {
20
+ minimum: minimum
21
+ }
22
+ end
23
+
24
+ def dump
25
+ Marshal.load(Marshal.dump(@data))
26
+ end
27
+
28
+ def minimum=(value)
29
+ protect!(value)
30
+
31
+ @minimum = value[:value]
32
+
33
+ @data[:minimum] = @minimum
34
+
35
+ minimum
36
+ end
37
+ end
38
+ end
39
+ end
@@ -2,42 +2,48 @@
2
2
 
3
3
  require_relative '../../satoshis'
4
4
  require_relative '../../rate'
5
-
6
- require_relative '../../../components/lnd'
5
+ require_relative '../../concerns/protectable'
6
+ require_relative 'htlc/blocks/delta'
7
7
 
8
8
  module Lighstorm
9
9
  module Models
10
10
  class HTLC
11
+ include Protectable
12
+
11
13
  def initialize(data)
12
14
  @data = data
13
15
  end
14
16
 
15
17
  def minimum
16
- @minimum ||= Satoshis.new(milisatoshis: @data[:minimum][:milisatoshis])
18
+ @minimum ||= if @data[:minimum]
19
+ Satoshis.new(
20
+ milisatoshis: @data[:minimum][:milisatoshis]
21
+ )
22
+ end
17
23
  end
18
24
 
19
25
  def maximum
20
- @maximum ||= Satoshis.new(milisatoshis: @data[:maximum][:milisatoshis])
26
+ @maximum ||= if @data[:maximum]
27
+ Satoshis.new(
28
+ milisatoshis: @data[:maximum][:milisatoshis]
29
+ )
30
+ end
21
31
  end
22
32
 
23
33
  def blocks
24
34
  @blocks ||= Struct.new(:data) do
25
35
  def delta
26
- Struct.new(:data) do
27
- def minimum
28
- data[:minimum]
29
- end
30
-
31
- def to_h
32
- { minimum: minimum }
33
- end
34
- end.new(data[:delta])
36
+ @delta ||= BlocksDelta.new(data[:delta] || {})
37
+ end
38
+
39
+ def dump
40
+ { delta: delta.dump }
35
41
  end
36
42
 
37
43
  def to_h
38
44
  { delta: delta.to_h }
39
45
  end
40
- end.new(@data[:blocks])
46
+ end.new(@data[:blocks] || {})
41
47
  end
42
48
 
43
49
  def to_h
@@ -47,6 +53,34 @@ module Lighstorm
47
53
  blocks: blocks.to_h
48
54
  }
49
55
  end
56
+
57
+ def dump
58
+ result = Marshal.load(Marshal.dump(@data))
59
+
60
+ result = result.merge({ blocks: blocks.dump }) if @data[:blocks]
61
+
62
+ result
63
+ end
64
+
65
+ def minimum=(value)
66
+ protect!(value)
67
+
68
+ @minimum = value[:value]
69
+
70
+ @data[:minimum] = { milisatoshis: @minimum.milisatoshis }
71
+
72
+ minimum
73
+ end
74
+
75
+ def maximum=(value)
76
+ protect!(value)
77
+
78
+ @maximum = value[:value]
79
+
80
+ @data[:maximum] = { milisatoshis: @maximum.milisatoshis }
81
+
82
+ maximum
83
+ end
50
84
  end
51
85
  end
52
86
  end
@@ -14,16 +14,25 @@ module Lighstorm
14
14
  end
15
15
 
16
16
  def fee
17
- @fee ||= @data ? Fee.new(self, @data[:fee]) : nil
17
+ @fee ||= Fee.new(self, @data ? @data[:fee] : {})
18
18
  end
19
19
 
20
20
  def htlc
21
- @htlc ||= @data ? HTLC.new(@data[:htlc]) : nil
21
+ @htlc ||= HTLC.new(@data ? @data[:htlc] : {})
22
22
  end
23
23
 
24
24
  def to_h
25
25
  { fee: fee.to_h, htlc: htlc.to_h }
26
26
  end
27
+
28
+ def dump
29
+ result = Marshal.load(Marshal.dump(@data))
30
+
31
+ result = result.merge({ fee: fee.dump }) if @data && @data[:fee]
32
+ result = result.merge({ htlc: htlc.dump }) if @data && @data[:htlc]
33
+
34
+ result
35
+ end
27
36
  end
28
37
  end
29
38
  end
@@ -9,12 +9,21 @@ require_relative '../errors'
9
9
  module Lighstorm
10
10
  module Models
11
11
  class ChannelNode
12
+ include Protectable
13
+
14
+ attr_reader :state
15
+
12
16
  def initialize(data, is_mine, transaction)
13
17
  @data = data
18
+ @state = data[:state]
14
19
  @is_mine = is_mine
15
20
  @transaction = transaction
16
21
  end
17
22
 
23
+ def active?
24
+ state == 'active'
25
+ end
26
+
18
27
  def node
19
28
  @node ||= Node.new(@data[:node])
20
29
  end
@@ -30,13 +39,36 @@ module Lighstorm
30
39
  end
31
40
 
32
41
  def to_h
33
- restult = { node: node.to_h }
42
+ restult = { state: state, node: node.to_h }
34
43
 
35
44
  restult[:accounting] = accounting.to_h if @is_mine
36
45
  restult[:policy] = policy.to_h if @data[:policy]
37
46
 
38
47
  restult
39
48
  end
49
+
50
+ def dump
51
+ result = Marshal.load(Marshal.dump(@data)).merge(
52
+ {
53
+ node: node.dump,
54
+ policy: policy.dump
55
+ }
56
+ )
57
+
58
+ result[:accounting] = accounting.dump if @is_mine
59
+ result.delete(:policy) if result[:policy].nil?
60
+
61
+ result
62
+ end
63
+
64
+ def state=(value)
65
+ protect!(value)
66
+
67
+ @state = value[:value]
68
+ @data[:state] = @state
69
+
70
+ state
71
+ end
40
72
  end
41
73
  end
42
74
  end
@@ -2,10 +2,13 @@
2
2
 
3
3
  require_relative '../../satoshis'
4
4
  require_relative '../../errors'
5
+ require_relative '../../concerns/protectable'
5
6
 
6
7
  module Lighstorm
7
8
  module Models
8
9
  class ChannelAccounting
10
+ include Protectable
11
+
9
12
  def initialize(data, is_mine)
10
13
  @data = data
11
14
  @is_mine = is_mine
@@ -63,6 +66,20 @@ module Lighstorm
63
66
  }
64
67
  end
65
68
  end
69
+
70
+ def dump
71
+ Marshal.load(Marshal.dump(@data))
72
+ end
73
+
74
+ def capacity=(value)
75
+ protect!(value)
76
+
77
+ @capacity = value[:value]
78
+
79
+ @data[:capacity][:milisatoshis] = @capacity.milisatoshis
80
+
81
+ capacity
82
+ end
66
83
  end
67
84
  end
68
85
  end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'time'
4
- require 'date'
5
4
 
6
5
  require_relative '../../ports/grpc'
7
6
  require_relative '../../adapters/edges/channel'
@@ -12,6 +11,7 @@ require_relative '../../components/cache'
12
11
  require_relative '../nodes/node'
13
12
  require_relative 'channel/accounting'
14
13
 
14
+ require_relative '../../controllers/channel/actions/apply_gossip'
15
15
  require_relative '../connections/channel_node'
16
16
  require_relative '../satoshis'
17
17
 
@@ -20,7 +20,7 @@ require_relative '../errors'
20
20
  module Lighstorm
21
21
  module Models
22
22
  class Channel
23
- attr_reader :data, :_key, :id, :opened_at, :up_at, :active, :exposure
23
+ attr_reader :data, :_key, :id
24
24
 
25
25
  def initialize(data)
26
26
  @data = data
@@ -57,10 +57,16 @@ module Lighstorm
57
57
  @data[:up_at]
58
58
  end
59
59
 
60
- def active
60
+ def active?
61
61
  ensure_mine!
62
62
 
63
- @data[:active]
63
+ @data[:state] == 'active'
64
+ end
65
+
66
+ def state
67
+ ensure_mine!
68
+
69
+ @data[:state]
64
70
  end
65
71
 
66
72
  def accounting
@@ -126,7 +132,7 @@ module Lighstorm
126
132
  id: id,
127
133
  opened_at: opened_at,
128
134
  up_at: up_at,
129
- active: active,
135
+ state: state,
130
136
  exposure: exposure,
131
137
  accounting: accounting.to_h,
132
138
  partner: partner.to_h,
@@ -148,6 +154,34 @@ module Lighstorm
148
154
  end
149
155
  end
150
156
 
157
+ def dump
158
+ result = Marshal.load(Marshal.dump(@data)).merge(
159
+ { partners: partners.map(&:dump) }
160
+ )
161
+
162
+ result[:accounting] = accounting.dump if known?
163
+
164
+ result
165
+ end
166
+
167
+ def self.adapt(gossip: nil, dump: nil)
168
+ raise TooManyArgumentsError, 'you need to pass gossip: or dump:, not both' if !gossip.nil? && !dump.nil?
169
+
170
+ raise ArgumentError, 'missing gossip: or dump:' if gossip.nil? && dump.nil?
171
+
172
+ if !gossip.nil?
173
+ new(Adapter::Channel.subscribe_channel_graph(gossip))
174
+ elsif !dump.nil?
175
+ new(dump)
176
+ end
177
+ end
178
+
179
+ def apply!(gossip:)
180
+ Controllers::Channel::ApplyGossip.perform(
181
+ self, gossip
182
+ )
183
+ end
184
+
151
185
  private
152
186
 
153
187
  def ensure_known!
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'time'
4
- require 'date'
5
4
 
6
5
  require_relative '../satoshis'
7
6
 
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'time'
4
- require 'date'
5
4
 
6
5
  require_relative '../satoshis'
7
6
 
data/models/errors.rb CHANGED
@@ -6,6 +6,9 @@ module Lighstorm
6
6
 
7
7
  class ToDoError < LighstormError; end
8
8
 
9
+ class TooManyArgumentsError < LighstormError; end
10
+ class IncoherentGossipError < LighstormError; end
11
+ class MissingGossipHandlerError < LighstormError; end
9
12
  class MissingCredentialsError < LighstormError; end
10
13
  class MissingMilisatoshisError < LighstormError; end
11
14
  class MissingPartsPerMillionError < LighstormError; end
@@ -20,6 +20,12 @@ module Lighstorm
20
20
  version: version
21
21
  }
22
22
  end
23
+
24
+ def dump
25
+ Marshal.load(
26
+ Marshal.dump({ implementation: implementation, version: version })
27
+ )
28
+ end
23
29
  end
24
30
  end
25
31
  end
@@ -37,6 +37,12 @@ module Lighstorm
37
37
 
38
38
  response
39
39
  end
40
+
41
+ def dump
42
+ data = Marshal.load(Marshal.dump(@data))
43
+ data.merge(lightning: lightning.dump) if @node.myself?
44
+ data
45
+ end
40
46
  end
41
47
  end
42
48
  end
data/models/nodes/node.rb CHANGED
@@ -1,6 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative '../../controllers/node/actions/apply_gossip'
3
4
  require_relative '../../controllers/channel'
5
+ require_relative '../../adapters/nodes/node'
6
+ require_relative '../concerns/protectable'
4
7
  require_relative '../errors'
5
8
 
6
9
  require_relative 'node/platform'
@@ -8,6 +11,8 @@ require_relative 'node/platform'
8
11
  module Lighstorm
9
12
  module Models
10
13
  class Node
14
+ include Protectable
15
+
11
16
  attr_reader :data, :_key, :alias, :public_key, :color
12
17
 
13
18
  def initialize(data)
@@ -46,6 +51,52 @@ module Lighstorm
46
51
 
47
52
  result
48
53
  end
54
+
55
+ def dump
56
+ result = Marshal.load(Marshal.dump(@data))
57
+
58
+ result[:platform] = platform.dump if @data[:platform]
59
+
60
+ result
61
+ end
62
+
63
+ def alias=(value)
64
+ protect!(value)
65
+
66
+ @alias = value[:value]
67
+
68
+ @data[:alias] = @alias
69
+
70
+ self.alias
71
+ end
72
+
73
+ def color=(value)
74
+ protect!(value)
75
+
76
+ @color = value[:value]
77
+
78
+ @data[:color] = @color
79
+
80
+ color
81
+ end
82
+
83
+ def self.adapt(gossip: nil, dump: nil)
84
+ raise TooManyArgumentsError, 'you need to pass gossip: or dump:, not both' if !gossip.nil? && !dump.nil?
85
+
86
+ raise ArgumentError, 'missing gossip: or dump:' if gossip.nil? && dump.nil?
87
+
88
+ if !gossip.nil?
89
+ new(Adapter::Node.subscribe_channel_graph(gossip))
90
+ elsif !dump.nil?
91
+ new(dump)
92
+ end
93
+ end
94
+
95
+ def apply!(gossip:)
96
+ Controllers::Node::ApplyGossip.perform(
97
+ self, gossip
98
+ )
99
+ end
49
100
  end
50
101
  end
51
102
  end
@@ -11,7 +11,10 @@ module Lighstorm
11
11
  def initialize(data)
12
12
  @data = data
13
13
 
14
- @_key = Digest::SHA256.hexdigest(data[:code])
14
+ @_key = data[:_key] || Digest::SHA256.hexdigest(
15
+ data[:code] || "#{data[:code][:amount][:milisatoshis]}#{Time.now}"
16
+ )
17
+
15
18
  @code = data[:code]
16
19
 
17
20
  @address = data[:address]
@@ -34,7 +37,7 @@ module Lighstorm
34
37
  def to_h
35
38
  { memo: memo, hash: hash }
36
39
  end
37
- end.new(@data[:description])
40
+ end.new(@data[:description] || {})
38
41
  end
39
42
 
40
43
  def secret
@@ -51,7 +54,7 @@ module Lighstorm
51
54
  # Don't expose 'secret' by default: Security
52
55
  { hash: hash }
53
56
  end
54
- end.new(@data[:secret])
57
+ end.new(@data[:secret] || {})
55
58
  end
56
59
 
57
60
  def to_h
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lighstorm
4
+ module Ports
5
+ class GRPCSession
6
+ attr_reader :calls
7
+
8
+ def initialize(grpc)
9
+ @grpc = grpc
10
+ @calls = {}
11
+ end
12
+
13
+ def handler(key, &block)
14
+ @calls[key] = 0 unless @calls.key?(key)
15
+ @calls[key] += 1
16
+ block.call
17
+ end
18
+
19
+ def method_missing(method_name, *args)
20
+ @grpc.send(method_name, *args) do |key, &block|
21
+ handler(key, &block)
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
data/ports/grpc.rb CHANGED
@@ -2,13 +2,15 @@
2
2
 
3
3
  require_relative '../components/cache'
4
4
  require_relative '../components/lnd'
5
+ require_relative 'grpc/session'
5
6
 
6
7
  module Lighstorm
7
8
  module Ports
8
9
  class GRPC
9
- def initialize(service, service_key)
10
+ def initialize(service, service_key, &handler)
10
11
  @service = service
11
12
  @service_key = service_key
13
+ @handler = handler
12
14
  end
13
15
 
14
16
  def call!(call_key, *args, &block)
@@ -17,12 +19,24 @@ module Lighstorm
17
19
  if block.nil?
18
20
  response = Cache.for(key, params: args&.first) do
19
21
  LND.instance.middleware(key) do
20
- @service.send(call_key, *args, &block)
22
+ if @handler
23
+ @handler.call(call_key) do
24
+ @service.send(call_key, *args, &block)
25
+ end
26
+ else
27
+ @service.send(call_key, *args, &block)
28
+ end
21
29
  end
22
30
  end
23
31
  else
24
32
  LND.instance.middleware(key) do
25
- @service.send(call_key, *args, &block)
33
+ if @handler
34
+ @handler.call(call_key) do
35
+ @service.send(call_key, *args, &block)
36
+ end
37
+ else
38
+ @service.send(call_key, *args, &block)
39
+ end
26
40
  end
27
41
  end
28
42
  end
@@ -41,7 +55,11 @@ module Lighstorm
41
55
  @service.respond_to?(call_key) || super
42
56
  end
43
57
 
44
- def self.method_missing(method_name, *_args)
58
+ def self.session
59
+ GRPCSession.new(self)
60
+ end
61
+
62
+ def self.method_missing(method_name, *_args, &block)
45
63
  service_key = method_name.to_sym
46
64
 
47
65
  unless LND.instance.client.respond_to?(service_key)
@@ -49,7 +67,7 @@ module Lighstorm
49
67
  "Method `#{method_name}` doesn't exist."
50
68
  end
51
69
 
52
- new(LND.instance.client.send(service_key), service_key)
70
+ new(LND.instance.client.send(service_key), service_key, &block)
53
71
  end
54
72
 
55
73
  def self.respond_to_missing?(method_name, include_private = false)
data/static/spec.rb CHANGED
@@ -4,7 +4,7 @@ module Lighstorm
4
4
  module Static
5
5
  SPEC = {
6
6
  name: 'lighstorm',
7
- version: '0.0.4',
7
+ version: '0.0.5',
8
8
  author: 'icebaker',
9
9
  summary: 'API for interacting with a Lightning Node.',
10
10
  description: 'Lighstorm is an opinionated abstraction layer on top of the lnd-client for interacting with a Lightning Node.',