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.
- checksums.yaml +7 -0
- data/.gitignore +1 -0
- data/.rubocop.yml +14 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +63 -0
- data/LICENSE +21 -0
- data/README.md +362 -0
- data/components/cache.rb +39 -0
- data/components/lnd.rb +55 -0
- data/lighstorm.gemspec +37 -0
- data/models/connections/channel_node/accounting.rb +30 -0
- data/models/connections/channel_node/constraints.rb +24 -0
- data/models/connections/channel_node/fee.rb +33 -0
- data/models/connections/channel_node/policy.rb +30 -0
- data/models/connections/channel_node.rb +41 -0
- data/models/connections/forward_channel.rb +54 -0
- data/models/connections/payment_channel.rb +58 -0
- data/models/edges/channel/accounting.rb +44 -0
- data/models/edges/channel.rb +179 -0
- data/models/edges/forward.rb +103 -0
- data/models/edges/payment.rb +251 -0
- data/models/nodes/node/lightning.rb +30 -0
- data/models/nodes/node/platform.rb +48 -0
- data/models/nodes/node.rb +81 -0
- data/models/rate.rb +23 -0
- data/models/satoshis.rb +58 -0
- data/ports/dsl/lighstorm.rb +31 -0
- data/static/spec.rb +15 -0
- metadata +122 -0
|
@@ -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
|