lighstorm 0.0.1

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.
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'fee'
4
+
5
+ module Lighstorm
6
+ module Models
7
+ class Policy
8
+ def initialize(channel, node)
9
+ @channel = channel
10
+ @node = node
11
+ end
12
+
13
+ def data
14
+ @data ||= if @channel.data[:get_chan_info].node1_pub == @node.public_key
15
+ @channel.data[:get_chan_info].node1_policy
16
+ else
17
+ @channel.data[:get_chan_info].node2_policy
18
+ end
19
+ end
20
+
21
+ def fees
22
+ @fees ||= Fee.new(self, @channel, @node)
23
+ end
24
+
25
+ def to_h
26
+ { fee: fees.to_h }
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'channel_node/constraints'
4
+ require_relative 'channel_node/policy'
5
+ require_relative 'channel_node/accounting'
6
+
7
+ module Lighstorm
8
+ module Models
9
+ class ChannelNode
10
+ KIND = :connection
11
+
12
+ attr_reader :node
13
+
14
+ def initialize(channel, node)
15
+ @channel = channel
16
+ @node = node
17
+ end
18
+
19
+ def policy
20
+ @policy ||= Policy.new(@channel, @node)
21
+ end
22
+
23
+ def constraints
24
+ @constraints ||= Constraints.new(@channel, @node)
25
+ end
26
+
27
+ def accounting
28
+ @accounting ||= ChannelNodeAccounting.new(@channel, @node)
29
+ end
30
+
31
+ def to_h
32
+ {
33
+ accounting: accounting.to_h,
34
+ node: @node.to_h,
35
+ policy: policy.to_h
36
+ # constraints: constraints.to_h
37
+ }
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../edges/channel'
4
+
5
+ module Lighstorm
6
+ module Models
7
+ class ForwardChannel
8
+ KIND = :connection
9
+
10
+ def initialize(direction, forward)
11
+ @direction = direction
12
+ @forward = forward
13
+ end
14
+
15
+ def channel
16
+ @channel ||= if @direction == :in
17
+ Channel.find_by_id(
18
+ @forward.data[:forwarding_history][:forwarding_events].first.chan_id_in
19
+ )
20
+ else
21
+ Channel.find_by_id(
22
+ @forward.data[:forwarding_history][:forwarding_events].first.chan_id_out
23
+ )
24
+ end
25
+ end
26
+
27
+ def amount
28
+ @amount ||= if @direction == :in
29
+ Satoshis.new(milisatoshis:
30
+ @forward.data[:forwarding_history][:forwarding_events].first.amt_in_msat)
31
+ else
32
+ Satoshis.new(milisatoshis:
33
+ @forward.data[:forwarding_history][:forwarding_events].first.amt_out_msat)
34
+ end
35
+ end
36
+
37
+ def to_h
38
+ {
39
+ amount: amount.to_h,
40
+ channel: {
41
+ id: channel.id,
42
+ partner: {
43
+ node: {
44
+ alias: channel.partner&.node&.alias,
45
+ public_key: channel.partner&.node&.public_key,
46
+ color: channel.partner&.node&.color
47
+ }
48
+ }
49
+ }
50
+ }
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../edges/channel'
4
+ require_relative '../nodes/node'
5
+
6
+ module Lighstorm
7
+ module Models
8
+ class PaymentChannel
9
+ KIND = :connection
10
+
11
+ def initialize(raw_hop, hop_index)
12
+ @raw_hop = raw_hop
13
+ @index = hop_index
14
+ end
15
+
16
+ def channel
17
+ @channel ||= Channel.find_by_id(@raw_hop.chan_id)
18
+ end
19
+
20
+ def amount
21
+ @amount ||= Satoshis.new(milisatoshis: @raw_hop.amt_to_forward_msat)
22
+ end
23
+
24
+ def fee
25
+ @fee ||= Satoshis.new(milisatoshis: @raw_hop.fee_msat)
26
+ end
27
+
28
+ def raw
29
+ @raw_hop
30
+ end
31
+
32
+ def partner_node
33
+ Node.find_by_public_key(@raw_hop.pub_key)
34
+ end
35
+
36
+ def to_h
37
+ {
38
+ hop: @index,
39
+ amount: amount.to_h,
40
+ fee: {
41
+ milisatoshis: fee.milisatoshis,
42
+ parts_per_million: fee.parts_per_million(amount.milisatoshis)
43
+ },
44
+ 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
+ }
52
+ }
53
+ }
54
+ }
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../satoshis'
4
+
5
+ module Lighstorm
6
+ module Models
7
+ class ChannelAccounting
8
+ def initialize(channel)
9
+ @channel = channel
10
+ end
11
+
12
+ def capacity
13
+ @capacity ||= Satoshis.new(milisatoshis: @channel.data[:get_chan_info].capacity * 1000)
14
+ end
15
+
16
+ def sent
17
+ @sent ||= Satoshis.new(milisatoshis: (
18
+ @channel.data[:list_channels][:channels].first.total_satoshis_sent.to_f * 1000.0
19
+ ))
20
+ end
21
+
22
+ def received
23
+ @received ||= Satoshis.new(milisatoshis: (
24
+ @channel.data[:list_channels][:channels].first.total_satoshis_received.to_f * 1000.0
25
+ ))
26
+ end
27
+
28
+ def unsettled
29
+ @unsettled ||= Satoshis.new(milisatoshis: (
30
+ @channel.data[:list_channels][:channels].first.unsettled_balance.to_f * 1000.0
31
+ ))
32
+ end
33
+
34
+ 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
+ }
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,179 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'time'
4
+ require 'date'
5
+
6
+ require_relative '../../components/lnd'
7
+ require_relative '../../components/cache'
8
+
9
+ require_relative '../nodes/node'
10
+ require_relative 'channel/accounting'
11
+
12
+ require_relative '../connections/channel_node'
13
+ require_relative '../satoshis'
14
+
15
+ module Lighstorm
16
+ module Models
17
+ class Channel
18
+ KIND = :edge
19
+
20
+ attr_reader :id, :data
21
+
22
+ def self.all
23
+ response = Cache.for('lightning.list_channels', ttl: 1) do
24
+ LND.instance.middleware('lightning.list_channels') do
25
+ LND.instance.client.lightning.list_channels
26
+ end
27
+ end
28
+
29
+ response.channels.map do |channel|
30
+ Channel.find_by_id(channel.chan_id)
31
+ end
32
+ end
33
+
34
+ def self.first
35
+ all.first
36
+ end
37
+
38
+ def self.last
39
+ all.last
40
+ end
41
+
42
+ def self.find_by_id(id)
43
+ Channel.new({ id: id })
44
+ end
45
+
46
+ 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
+ end
55
+ end
56
+
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
+ end
63
+
64
+ fetch_from_fee_report!
65
+
66
+ fetch_from_list_channels!
67
+ calculate_times_after_list_channels!
68
+ end
69
+
70
+ def active
71
+ @data[:list_channels] ? @data[:list_channels][:channels].first.active : nil
72
+ end
73
+
74
+ def exposure
75
+ return unless @data[:list_channels]
76
+
77
+ @data[:list_channels][:channels].first.private ? 'private' : 'public'
78
+ end
79
+
80
+ def opened_at
81
+ @opened_at ||= if @data[:list_channels]
82
+ DateTime.parse(
83
+ (Time.now - @data[:list_channels][:channels].first.lifetime).to_s
84
+ )
85
+ end
86
+ end
87
+
88
+ def up_at
89
+ @up_at ||= if @data[:list_channels]
90
+ DateTime.parse(
91
+ (Time.now - @data[:list_channels][:channels].first.uptime).to_s
92
+ )
93
+ end
94
+ end
95
+
96
+ def accounting
97
+ return nil unless @data[:get_chan_info]
98
+
99
+ @accounting ||= ChannelAccounting.new(self)
100
+ end
101
+
102
+ def myself
103
+ return nil unless @data[:get_chan_info]
104
+
105
+ @myself ||= ChannelNode.new(self, Node.myself)
106
+ end
107
+
108
+ def partner
109
+ return nil unless @data[:get_chan_info]
110
+
111
+ public_key = if @data[:get_chan_info].node1_pub == myself.node.public_key
112
+ @data[:get_chan_info].node2_pub
113
+ else
114
+ @data[:get_chan_info].node1_pub
115
+ end
116
+
117
+ @partner ||= ChannelNode.new(self, Node.find_by_public_key(public_key))
118
+ end
119
+
120
+ def raw
121
+ {
122
+ get_chan_info: @data[:get_chan_info].to_h,
123
+ list_channels: { channels: @data[:list_channels][:channels].map(&:to_h) }
124
+ }
125
+ end
126
+
127
+ 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
+ }
138
+ end
139
+
140
+ private
141
+
142
+ # Ensure that we are getting fresh up-date data about our own fees.
143
+ def fetch_from_fee_report!
144
+ response = Cache.for('lightning.fee_report', ttl: 1) do
145
+ LND.instance.middleware('lightning.fee_report') do
146
+ LND.instance.client.lightning.fee_report
147
+ end
148
+ end
149
+
150
+ response.channel_fees.map do |channel|
151
+ if channel.chan_id == @id
152
+ @data[:fee_report] = { channel_fees: [channel] }
153
+ break
154
+ end
155
+ end
156
+ end
157
+
158
+ def fetch_from_list_channels!
159
+ response = Cache.for('lightning.list_channels', ttl: 1) do
160
+ LND.instance.middleware('lightning.list_channels') do
161
+ LND.instance.client.lightning.list_channels
162
+ end
163
+ end
164
+
165
+ response.channels.map do |channel|
166
+ if channel.chan_id == @id
167
+ @data[:list_channels] = { channels: [channel] }
168
+ break
169
+ end
170
+ end
171
+ end
172
+
173
+ def calculate_times_after_list_channels!
174
+ opened_at
175
+ up_at
176
+ end
177
+ end
178
+ end
179
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'digest'
4
+ require 'time'
5
+ require 'date'
6
+
7
+ require_relative '../satoshis'
8
+
9
+ require_relative '../connections/forward_channel'
10
+
11
+ module Lighstorm
12
+ module Models
13
+ class Forward
14
+ KIND = :edge
15
+
16
+ attr_reader :data
17
+
18
+ def self.all(limit: nil)
19
+ last_offset = 0
20
+
21
+ forwards = []
22
+
23
+ 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
+ )
28
+ end
29
+
30
+ response.forwarding_events.each { |raw_forward| forwards << raw_forward }
31
+
32
+ # Unfortunately, forwards aren't sorted in descending order. :(
33
+ # break if !limit.nil? && forwards.size >= limit
34
+
35
+ break if last_offset == response.last_offset_index || last_offset > response.last_offset_index
36
+
37
+ last_offset = response.last_offset_index
38
+ end
39
+
40
+ forwards = forwards.sort_by { |raw_forward| -raw_forward.timestamp_ns }
41
+
42
+ forwards = forwards[0..limit - 1] unless limit.nil?
43
+
44
+ forwards.map { |raw_forward| Forward.new(raw_forward) }
45
+ end
46
+
47
+ def self.first
48
+ all(limit: 1).first
49
+ end
50
+
51
+ def self.last
52
+ all.last
53
+ end
54
+
55
+ def initialize(raw)
56
+ @data = { forwarding_history: { forwarding_events: [raw] } }
57
+ end
58
+
59
+ def id
60
+ @id ||= Digest::SHA256.hexdigest(
61
+ @data[:forwarding_history][:forwarding_events].first.timestamp_ns.to_s
62
+ )
63
+ end
64
+
65
+ def at
66
+ DateTime.parse(Time.at(@data[:forwarding_history][:forwarding_events].first.timestamp).to_s)
67
+ end
68
+
69
+ def fee
70
+ Satoshis.new(milisatoshis: @data[:forwarding_history][:forwarding_events].first.fee_msat)
71
+ end
72
+
73
+ def in
74
+ @in ||= ForwardChannel.new(:in, self)
75
+ end
76
+
77
+ def out
78
+ @out ||= ForwardChannel.new(:out, self)
79
+ end
80
+
81
+ def to_h
82
+ {
83
+ id: id,
84
+ at: at,
85
+ fee: {
86
+ milisatoshis: fee.milisatoshis,
87
+ parts_per_million: fee.parts_per_million(self.in.amount.milisatoshis)
88
+ },
89
+ in: self.in.to_h,
90
+ out: out.to_h
91
+ }
92
+ end
93
+
94
+ def raw
95
+ {
96
+ forwarding_history: {
97
+ forwarding_events: [@data[:forwarding_history][:forwarding_events].first.to_h]
98
+ }
99
+ }
100
+ end
101
+ end
102
+ end
103
+ end