lighstorm 0.0.1 → 0.0.3

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.
@@ -8,13 +8,16 @@ module Lighstorm
8
8
  class PaymentChannel
9
9
  KIND = :connection
10
10
 
11
- def initialize(raw_hop, hop_index)
11
+ def initialize(raw_hop, hop_index, respond_info: true)
12
+ @respond_info = respond_info
12
13
  @raw_hop = raw_hop
13
- @index = hop_index
14
+ @hop = hop_index
14
15
  end
15
16
 
17
+ attr_reader :hop
18
+
16
19
  def channel
17
- @channel ||= Channel.find_by_id(@raw_hop.chan_id)
20
+ Channel.find_by_id(@raw_hop.chan_id)
18
21
  end
19
22
 
20
23
  def amount
@@ -34,24 +37,35 @@ module Lighstorm
34
37
  end
35
38
 
36
39
  def to_h
37
- {
38
- hop: @index,
40
+ response = {
41
+ hop: hop,
39
42
  amount: amount.to_h,
40
43
  fee: {
41
44
  milisatoshis: fee.milisatoshis,
42
45
  parts_per_million: fee.parts_per_million(amount.milisatoshis)
43
46
  },
44
47
  channel: {
45
- id: channel.id,
46
- partner: {
47
- node: {
48
- alias: partner_node&.alias,
49
- public_key: partner_node&.public_key,
50
- color: partner_node&.color
51
- }
48
+ id: @raw_hop.chan_id.to_s,
49
+ node: {
50
+ public_key: @raw_hop.pub_key
52
51
  }
53
52
  }
54
53
  }
54
+
55
+ return response unless @respond_info
56
+
57
+ response[:channel] = {
58
+ id: channel.id,
59
+ partner: {
60
+ node: {
61
+ alias: partner_node&.alias,
62
+ public_key: partner_node&.public_key,
63
+ color: partner_node&.color
64
+ }
65
+ }
66
+ }
67
+
68
+ response
55
69
  end
56
70
  end
57
71
  end
@@ -10,34 +10,50 @@ module Lighstorm
10
10
  end
11
11
 
12
12
  def capacity
13
- @capacity ||= Satoshis.new(milisatoshis: @channel.data[:get_chan_info].capacity * 1000)
13
+ if @channel.data[:get_chan_info]
14
+ @capacity ||= Satoshis.new(milisatoshis: @channel.data[:get_chan_info].capacity * 1000)
15
+ elsif @channel.data[:describe_graph]
16
+ @capacity ||= Satoshis.new(milisatoshis: @channel.data[:describe_graph].capacity * 1000)
17
+ end
14
18
  end
15
19
 
16
20
  def sent
21
+ return nil unless @channel.data[:list_channels]
22
+
17
23
  @sent ||= Satoshis.new(milisatoshis: (
18
24
  @channel.data[:list_channels][:channels].first.total_satoshis_sent.to_f * 1000.0
19
25
  ))
20
26
  end
21
27
 
22
28
  def received
29
+ return nil unless @channel.data[:list_channels]
30
+
23
31
  @received ||= Satoshis.new(milisatoshis: (
24
32
  @channel.data[:list_channels][:channels].first.total_satoshis_received.to_f * 1000.0
25
33
  ))
26
34
  end
27
35
 
28
36
  def unsettled
37
+ return nil unless @channel.data[:list_channels]
38
+
29
39
  @unsettled ||= Satoshis.new(milisatoshis: (
30
40
  @channel.data[:list_channels][:channels].first.unsettled_balance.to_f * 1000.0
31
41
  ))
32
42
  end
33
43
 
34
44
  def to_h
35
- {
36
- capacity: capacity.to_h,
37
- sent: sent.to_h,
38
- received: received.to_h,
39
- unsettled: unsettled.to_h
40
- }
45
+ if @channel.data[:get_chan_info]
46
+ {
47
+ capacity: capacity.to_h,
48
+ sent: sent.to_h,
49
+ received: received.to_h,
50
+ unsettled: unsettled.to_h
51
+ }
52
+ else
53
+ {
54
+ capacity: capacity.to_h
55
+ }
56
+ end
41
57
  end
42
58
  end
43
59
  end
@@ -17,48 +17,57 @@ module Lighstorm
17
17
  class Channel
18
18
  KIND = :edge
19
19
 
20
- attr_reader :id, :data
20
+ attr_reader :data
21
21
 
22
22
  def self.all
23
- response = Cache.for('lightning.list_channels', ttl: 1) do
23
+ response = LND.instance.middleware('lightning.describe_graph') do
24
+ LND.instance.client.lightning.describe_graph
25
+ end
26
+
27
+ response.edges.map do |raw_channel|
28
+ Channel.new({ describe_graph: raw_channel })
29
+ end
30
+ end
31
+
32
+ def self.mine
33
+ response = Cache.for('lightning.list_channels') do
24
34
  LND.instance.middleware('lightning.list_channels') do
25
35
  LND.instance.client.lightning.list_channels
26
36
  end
27
37
  end
28
38
 
29
39
  response.channels.map do |channel|
30
- Channel.find_by_id(channel.chan_id)
40
+ Channel.find_by_id(channel.chan_id.to_s)
31
41
  end
32
42
  end
33
43
 
34
- def self.first
35
- all.first
36
- end
37
-
38
- def self.last
39
- all.last
40
- end
41
-
42
44
  def self.find_by_id(id)
43
45
  Channel.new({ id: id })
44
46
  end
45
47
 
48
+ def id
49
+ # Standard JSON don't support BigInt, so, a String is safer.
50
+ @id.to_s
51
+ end
52
+
46
53
  def initialize(params)
47
- begin
48
- response = Cache.for(
49
- 'lightning.get_chan_info',
50
- ttl: 1, params: { chan_id: params[:id] }
51
- ) do
52
- LND.instance.middleware('lightning.get_chan_info') do
53
- LND.instance.client.lightning.get_chan_info(chan_id: params[:id])
54
+ if params[:id]
55
+ begin
56
+ response = Cache.for('lightning.get_chan_info', params: { chan_id: params[:id].to_i }) do
57
+ LND.instance.middleware('lightning.get_chan_info') do
58
+ LND.instance.client.lightning.get_chan_info(chan_id: params[:id].to_i)
59
+ end
54
60
  end
55
- end
56
61
 
57
- @data = { get_chan_info: response }
58
- @id = @data[:get_chan_info].channel_id
59
- rescue StandardError => _e
60
- @data = { get_chan_info: nil }
61
- @id = params[:id]
62
+ @data = { get_chan_info: response }
63
+ @id = @data[:get_chan_info].channel_id
64
+ rescue StandardError => e
65
+ @data = { get_chan_info: nil, error: e }
66
+ @id = params[:id]
67
+ end
68
+ elsif params[:describe_graph]
69
+ @data = { describe_graph: params[:describe_graph] }
70
+ @id = @data[:describe_graph].channel_id
62
71
  end
63
72
 
64
73
  fetch_from_fee_report!
@@ -67,11 +76,21 @@ module Lighstorm
67
76
  calculate_times_after_list_channels!
68
77
  end
69
78
 
79
+ def error?
80
+ !@data[:error].nil?
81
+ end
82
+
83
+ def error
84
+ @data[:error]
85
+ end
86
+
70
87
  def active
71
88
  @data[:list_channels] ? @data[:list_channels][:channels].first.active : nil
72
89
  end
73
90
 
74
91
  def exposure
92
+ return 'public' if @data[:describe_graph]
93
+
75
94
  return unless @data[:list_channels]
76
95
 
77
96
  @data[:list_channels][:channels].first.private ? 'private' : 'public'
@@ -94,24 +113,70 @@ module Lighstorm
94
113
  end
95
114
 
96
115
  def accounting
97
- return nil unless @data[:get_chan_info]
98
-
99
116
  @accounting ||= ChannelAccounting.new(self)
100
117
  end
101
118
 
119
+ def partners(fetch: false)
120
+ @partners ||= if mine?
121
+ [myself, partner]
122
+ elsif @data[:describe_graph]
123
+ [
124
+ ChannelNode.new(
125
+ self,
126
+ Node.new({ public_key: @data[:describe_graph].node1_pub }, fetch: fetch)
127
+ ),
128
+ ChannelNode.new(
129
+ self,
130
+ Node.new({ public_key: @data[:describe_graph].node2_pub }, fetch: fetch)
131
+ )
132
+ ]
133
+ elsif @data[:get_chan_info]
134
+ [
135
+ ChannelNode.new(
136
+ self,
137
+ Node.new({ public_key: @data[:get_chan_info].node1_pub }, fetch: fetch)
138
+ ),
139
+ ChannelNode.new(
140
+ self,
141
+ Node.new({ public_key: @data[:get_chan_info].node2_pub }, fetch: fetch)
142
+ )
143
+ ]
144
+ else
145
+ raise 'missing data'
146
+ end
147
+ end
148
+
149
+ def mine?
150
+ my_node = Node.myself.public_key
151
+
152
+ if @data[:get_chan_info]
153
+ (
154
+ @data[:get_chan_info].node1_pub == my_node || @data[:get_chan_info].node2_pub == my_node
155
+ )
156
+ elsif @data[:describe_graph]
157
+ (
158
+ @data[:describe_graph].node1_pub == my_node || @data[:describe_graph].node2_pub == my_node
159
+ )
160
+ else
161
+ false
162
+ end
163
+ end
164
+
102
165
  def myself
103
- return nil unless @data[:get_chan_info]
166
+ raise 'not your channel' unless mine?
104
167
 
105
168
  @myself ||= ChannelNode.new(self, Node.myself)
106
169
  end
107
170
 
108
171
  def partner
109
- return nil unless @data[:get_chan_info]
172
+ raise 'not your channel' unless mine?
173
+
174
+ key = @data[:get_chan_info] ? :get_chan_info : :describe_graph
110
175
 
111
- public_key = if @data[:get_chan_info].node1_pub == myself.node.public_key
112
- @data[:get_chan_info].node2_pub
176
+ public_key = if @data[key].node1_pub == myself.node.public_key
177
+ @data[key].node2_pub
113
178
  else
114
- @data[:get_chan_info].node1_pub
179
+ @data[key].node1_pub
115
180
  end
116
181
 
117
182
  @partner ||= ChannelNode.new(self, Node.find_by_public_key(public_key))
@@ -120,28 +185,39 @@ module Lighstorm
120
185
  def raw
121
186
  {
122
187
  get_chan_info: @data[:get_chan_info].to_h,
123
- list_channels: { channels: @data[:list_channels][:channels].map(&:to_h) }
188
+ describe_graph: @data[:describe_graph].to_h,
189
+ list_channels: {
190
+ channels: @data[:list_channels] ? @data[:list_channels][:channels].map(&:to_h) : nil
191
+ }
124
192
  }
125
193
  end
126
194
 
127
195
  def to_h
128
- {
129
- id: id,
130
- opened_at: opened_at,
131
- up_at: up_at,
132
- active: active,
133
- exposure: exposure,
134
- accounting: accounting.to_h,
135
- partner: partner.to_h,
136
- myself: myself.to_h
137
- }
196
+ if @data[:get_chan_info]
197
+ {
198
+ id: id,
199
+ opened_at: opened_at,
200
+ up_at: up_at,
201
+ active: active,
202
+ exposure: exposure,
203
+ accounting: accounting.to_h,
204
+ partner: partner.to_h,
205
+ myself: myself.to_h
206
+ }
207
+ else
208
+ {
209
+ id: id,
210
+ accounting: accounting.to_h,
211
+ partners: partners.map(&:to_h)
212
+ }
213
+ end
138
214
  end
139
215
 
140
216
  private
141
217
 
142
218
  # Ensure that we are getting fresh up-date data about our own fees.
143
219
  def fetch_from_fee_report!
144
- response = Cache.for('lightning.fee_report', ttl: 1) do
220
+ response = Cache.for('lightning.fee_report') do
145
221
  LND.instance.middleware('lightning.fee_report') do
146
222
  LND.instance.client.lightning.fee_report
147
223
  end
@@ -156,7 +232,7 @@ module Lighstorm
156
232
  end
157
233
 
158
234
  def fetch_from_list_channels!
159
- response = Cache.for('lightning.list_channels', ttl: 1) do
235
+ response = Cache.for('lightning.list_channels') do
160
236
  LND.instance.middleware('lightning.list_channels') do
161
237
  LND.instance.client.lightning.list_channels
162
238
  end
@@ -7,6 +7,7 @@ require 'date'
7
7
  require_relative '../satoshis'
8
8
 
9
9
  require_relative '../connections/forward_channel'
10
+ require_relative 'groups/channel_forwards'
10
11
 
11
12
  module Lighstorm
12
13
  module Models
@@ -15,16 +16,21 @@ module Lighstorm
15
16
 
16
17
  attr_reader :data
17
18
 
18
- def self.all(limit: nil)
19
+ def self.all(limit: nil, raw: false, info: true)
19
20
  last_offset = 0
20
21
 
21
22
  forwards = []
22
23
 
23
24
  loop do
24
- response = LND.instance.middleware('lightning.forwarding_history') do
25
- LND.instance.client.lightning.forwarding_history(
26
- peer_alias_lookup: true, index_offset: last_offset
27
- )
25
+ response = Cache.for(
26
+ 'lightning.forwarding_history',
27
+ params: { peer_alias_lookup: true, index_offset: last_offset }
28
+ ) do
29
+ LND.instance.middleware('lightning.forwarding_history') do
30
+ LND.instance.client.lightning.forwarding_history(
31
+ peer_alias_lookup: true, index_offset: last_offset
32
+ )
33
+ end
28
34
  end
29
35
 
30
36
  response.forwarding_events.each { |raw_forward| forwards << raw_forward }
@@ -41,7 +47,9 @@ module Lighstorm
41
47
 
42
48
  forwards = forwards[0..limit - 1] unless limit.nil?
43
49
 
44
- forwards.map { |raw_forward| Forward.new(raw_forward) }
50
+ return forwards if raw
51
+
52
+ forwards.map { |raw_forward| Forward.new(raw_forward, respond_info: info) }
45
53
  end
46
54
 
47
55
  def self.first
@@ -52,7 +60,52 @@ module Lighstorm
52
60
  all.last
53
61
  end
54
62
 
55
- def initialize(raw)
63
+ def self.group_by_channel(direction: :out, hours_ago: nil, limit: nil, info: true)
64
+ raw_forwards = all(raw: true)
65
+
66
+ direction = direction.to_sym
67
+
68
+ groups = {}
69
+
70
+ raw_forwards.each do |raw_forward|
71
+ channel_id = direction == :in ? raw_forward.chan_id_in : raw_forward.chan_id_out
72
+
73
+ if hours_ago
74
+ forward_hours_ago = (
75
+ Time.now - Time.at(raw_forward.timestamp_ns / 1e+9)
76
+ ).to_f / 3600
77
+
78
+ next if forward_hours_ago > hours_ago
79
+ end
80
+
81
+ unless groups[channel_id]
82
+ groups[channel_id] = {
83
+ last_at: nil,
84
+ analysis: { count: 0, sums: { amount: 0, fee: 0 } },
85
+ direction => { id: channel_id }
86
+ }
87
+ end
88
+
89
+ groups[channel_id][:analysis][:count] += 1
90
+ groups[channel_id][:analysis][:sums][:amount] += raw_forward.amt_in_msat
91
+ groups[channel_id][:analysis][:sums][:fee] += raw_forward.fee_msat
92
+
93
+ if groups[channel_id][:last_at].nil? || raw_forward.timestamp_ns > groups[channel_id][:last_at]
94
+ groups[channel_id][:last_at] = raw_forward.timestamp_ns
95
+ groups[channel_id][:sample] = raw_forward
96
+ end
97
+ end
98
+
99
+ groups = groups.values.sort_by { |group| - group[:last_at] }
100
+ .sort_by { |group| - group[:analysis][:count] }
101
+
102
+ groups = groups[0..limit - 1] unless limit.nil?
103
+
104
+ groups.map { |raw_group| ChannelForwardsGroup.new(direction, raw_group) }
105
+ end
106
+
107
+ def initialize(raw, respond_info: true)
108
+ @respond_info = respond_info
56
109
  @data = { forwarding_history: { forwarding_events: [raw] } }
57
110
  end
58
111
 
@@ -63,7 +116,9 @@ module Lighstorm
63
116
  end
64
117
 
65
118
  def at
66
- DateTime.parse(Time.at(@data[:forwarding_history][:forwarding_events].first.timestamp).to_s)
119
+ DateTime.parse(Time.at(
120
+ @data[:forwarding_history][:forwarding_events].first.timestamp_ns / 1e+9
121
+ ).to_s)
67
122
  end
68
123
 
69
124
  def fee
@@ -71,11 +126,11 @@ module Lighstorm
71
126
  end
72
127
 
73
128
  def in
74
- @in ||= ForwardChannel.new(:in, self)
129
+ @in ||= ForwardChannel.new(:in, self, respond_info: @respond_info)
75
130
  end
76
131
 
77
132
  def out
78
- @out ||= ForwardChannel.new(:out, self)
133
+ @out ||= ForwardChannel.new(:out, self, respond_info: @respond_info)
79
134
  end
80
135
 
81
136
  def to_h
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lighstorm
4
+ module Models
5
+ class ChannelForwardsGroup
6
+ Analysis = Struct.new(:analysis) do
7
+ def count
8
+ analysis[:count]
9
+ end
10
+
11
+ def sums
12
+ Struct.new(:sums) do
13
+ def amount
14
+ Satoshis.new(milisatoshis: sums[:amount])
15
+ end
16
+
17
+ def fee
18
+ Satoshis.new(milisatoshis: sums[:fee])
19
+ end
20
+
21
+ def to_h
22
+ {
23
+ amount: amount.to_h,
24
+ fee: {
25
+ milisatoshis: fee.milisatoshis,
26
+ parts_per_million: fee.parts_per_million(amount.milisatoshis)
27
+ }
28
+ }
29
+ end
30
+ end.new(analysis[:sums])
31
+ end
32
+
33
+ def averages
34
+ Struct.new(:analysis) do
35
+ def amount
36
+ Satoshis.new(
37
+ milisatoshis: analysis[:sums][:amount].to_f / analysis[:count]
38
+ )
39
+ end
40
+
41
+ def fee
42
+ Satoshis.new(
43
+ milisatoshis: analysis[:sums][:fee].to_f / analysis[:count]
44
+ )
45
+ end
46
+
47
+ def to_h
48
+ {
49
+ amount: amount.to_h,
50
+ fee: {
51
+ milisatoshis: fee.milisatoshis,
52
+ parts_per_million: fee.parts_per_million(amount.milisatoshis)
53
+ }
54
+ }
55
+ end
56
+ end.new(analysis)
57
+ end
58
+
59
+ def to_h
60
+ {
61
+ count: count,
62
+ sums: sums.to_h,
63
+ averages: averages.to_h
64
+ }
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../satoshis'
4
+ require_relative 'analysis'
5
+
6
+ module Lighstorm
7
+ module Models
8
+ class ChannelForwardsGroup
9
+ def initialize(direction, data)
10
+ @direction = direction
11
+ @data = data
12
+ end
13
+
14
+ def last_at
15
+ @last_at ||= DateTime.parse(Time.at(@data[:last_at].to_f / 1e+9).to_s)
16
+ end
17
+
18
+ def analysis
19
+ Analysis.new(@data[:analysis])
20
+ end
21
+
22
+ def in
23
+ return @in if @in
24
+
25
+ raise raise ArgumentError, "Method `in` doesn't exist." unless @direction == :in
26
+
27
+ @in = Channel.new({ id: @data[:in][:id] })
28
+ end
29
+
30
+ def out
31
+ return @out if @out
32
+
33
+ raise raise ArgumentError, "Method `out` doesn't exist." unless @direction == :out
34
+
35
+ @out = Channel.new({ id: @data[:out][:id] })
36
+ end
37
+
38
+ # def capacity
39
+ # @capacity ||= Satoshis.new(milisatoshis: @channel.data[:get_chan_info].capacity * 1000)
40
+ # end
41
+
42
+ def to_h
43
+ {
44
+ last_at: last_at,
45
+ analysis: analysis.to_h,
46
+ @direction => {
47
+ id: channel.id,
48
+ partner: {
49
+ node: {
50
+ alias: channel&.partner&.node&.alias,
51
+ public_key: channel&.partner&.node&.public_key,
52
+ color: channel&.partner&.node&.color
53
+ }
54
+ }
55
+ }
56
+ }
57
+ end
58
+
59
+ private
60
+
61
+ def channel
62
+ @channel ||= @direction == :in ? self.in : out
63
+ end
64
+ end
65
+ end
66
+ end
@@ -15,14 +15,16 @@ module Lighstorm
15
15
 
16
16
  attr_reader :data
17
17
 
18
- def self.all(limit: nil, purpose: nil, hops: true)
18
+ def self.all(limit: nil, purpose: nil, hops: true, info: true)
19
19
  last_offset = 0
20
20
 
21
21
  payments = []
22
22
 
23
23
  loop do
24
- response = LND.instance.middleware('lightning.list_payments') do
25
- LND.instance.client.lightning.list_payments(index_offset: last_offset)
24
+ response = Cache.for('lightning.list_payments', params: { index_offset: last_offset }) do
25
+ LND.instance.middleware('lightning.list_payments') do
26
+ LND.instance.client.lightning.list_payments(index_offset: last_offset)
27
+ end
26
28
  end
27
29
 
28
30
  response.payments.each do |raw_payment|
@@ -57,7 +59,7 @@ module Lighstorm
57
59
  payments = payments[0..limit - 1] unless limit.nil?
58
60
 
59
61
  payments.map do |raw_payment|
60
- Payment.new(raw_payment, respond_hops: hops)
62
+ Payment.new(raw_payment, respond_hops: hops, respond_info: info)
61
63
  end
62
64
  end
63
65
 
@@ -69,8 +71,9 @@ module Lighstorm
69
71
  all.last
70
72
  end
71
73
 
72
- def initialize(raw, respond_hops: true)
74
+ def initialize(raw, respond_hops: true, respond_info: true)
73
75
  @respond_hops = respond_hops
76
+ @respond_info = respond_info
74
77
  @data = { list_payments: { payments: [raw] } }
75
78
  end
76
79
 
@@ -163,7 +166,7 @@ module Lighstorm
163
166
 
164
167
  @from = PaymentChannel.new(
165
168
  @data[:list_payments][:payments].first.htlcs.first.route.hops.first,
166
- 1
169
+ 1, respond_info: @respond_info
167
170
  )
168
171
 
169
172
  @from
@@ -185,12 +188,14 @@ module Lighstorm
185
188
  @data[:list_payments][:payments].first.htlcs.first.route.hops[
186
189
  @data[:list_payments][:payments].first.htlcs.first.route.hops.size - 2
187
190
  ],
188
- @data[:list_payments][:payments].first.htlcs.first.route.hops.size - 1
191
+ @data[:list_payments][:payments].first.htlcs.first.route.hops.size - 1,
192
+ respond_info: @respond_info
189
193
  )
190
194
  else
191
195
  PaymentChannel.new(
192
196
  @data[:list_payments][:payments].first.htlcs.first.route.hops.last,
193
- @data[:list_payments][:payments].first.htlcs.first.route.hops.size
197
+ @data[:list_payments][:payments].first.htlcs.first.route.hops.size,
198
+ respond_info: @respond_info
194
199
  )
195
200
  end
196
201
 
@@ -203,7 +208,7 @@ module Lighstorm
203
208
  validated_htlcs_number!
204
209
 
205
210
  @hops = @data[:list_payments][:payments].first.htlcs.first.route.hops.map.with_index do |raw_hop, i|
206
- PaymentChannel.new(raw_hop, i + 1)
211
+ PaymentChannel.new(raw_hop, i + 1, respond_info: @respond_info)
207
212
  end
208
213
  end
209
214