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
|