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,251 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'time'
4
+ require 'date'
5
+
6
+ require_relative '../satoshis'
7
+
8
+ require_relative '../connections/payment_channel'
9
+ require_relative '../nodes/node'
10
+
11
+ module Lighstorm
12
+ module Models
13
+ class Payment
14
+ KIND = :edge
15
+
16
+ attr_reader :data
17
+
18
+ def self.all(limit: nil, purpose: nil, hops: true)
19
+ last_offset = 0
20
+
21
+ payments = []
22
+
23
+ loop do
24
+ response = LND.instance.middleware('lightning.list_payments') do
25
+ LND.instance.client.lightning.list_payments(index_offset: last_offset)
26
+ end
27
+
28
+ response.payments.each do |raw_payment|
29
+ case purpose
30
+ when 'potential-submarine', 'submarine'
31
+ payments << raw_payment if raw_potential_submarine?(raw_payment)
32
+ when '!potential-submarine', '!submarine'
33
+ payments << raw_payment unless raw_potential_submarine?(raw_payment)
34
+ when 'rebalance'
35
+ payments << raw_payment if raw_rebalance?(raw_payment)
36
+ when '!rebalance'
37
+ payments << raw_payment unless raw_rebalance?(raw_payment)
38
+ when '!payment'
39
+ payments << raw_payment if raw_potential_submarine?(raw_payment) || raw_rebalance?(raw_payment)
40
+ when 'payment'
41
+ payments << raw_payment if !raw_potential_submarine?(raw_payment) && !raw_rebalance?(raw_payment)
42
+ else
43
+ payments << raw_payment
44
+ end
45
+ end
46
+
47
+ # Fortunately, payments are sorted in descending order. :)
48
+ break if !limit.nil? && payments.size >= limit
49
+
50
+ break if last_offset == response.last_index_offset || last_offset > response.last_index_offset
51
+
52
+ last_offset = response.last_index_offset
53
+ end
54
+
55
+ payments = payments.sort_by { |raw_payment| -raw_payment.creation_time_ns }
56
+
57
+ payments = payments[0..limit - 1] unless limit.nil?
58
+
59
+ payments.map do |raw_payment|
60
+ Payment.new(raw_payment, respond_hops: hops)
61
+ end
62
+ end
63
+
64
+ def self.first
65
+ all(limit: 1).first
66
+ end
67
+
68
+ def self.last
69
+ all.last
70
+ end
71
+
72
+ def initialize(raw, respond_hops: true)
73
+ @respond_hops = respond_hops
74
+ @data = { list_payments: { payments: [raw] } }
75
+ end
76
+
77
+ def id
78
+ @id ||= @data[:list_payments][:payments].first.payment_hash
79
+ end
80
+
81
+ def hash
82
+ @hash ||= @data[:list_payments][:payments].first.payment_hash
83
+ end
84
+
85
+ def status
86
+ @status ||= @data[:list_payments][:payments].first.status
87
+ end
88
+
89
+ def created_at
90
+ @created_at ||= DateTime.parse(Time.at(@data[:list_payments][:payments].first.creation_date).to_s)
91
+ end
92
+
93
+ def amount
94
+ @amount ||= Satoshis.new(
95
+ milisatoshis: @data[:list_payments][:payments].first.value_msat
96
+ )
97
+ end
98
+
99
+ def fee
100
+ @fee ||= Satoshis.new(
101
+ milisatoshis: @data[:list_payments][:payments].first.fee_msat
102
+ )
103
+ end
104
+
105
+ def purpose
106
+ @purpose ||= Payment.raw_purpose(@data[:list_payments][:payments].first)
107
+ end
108
+
109
+ def rebalance?
110
+ return @rebalance unless @rebalance.nil?
111
+
112
+ validated_htlcs_number!
113
+
114
+ @rebalance = Payment.raw_rebalance?(
115
+ @data[:list_payments][:payments].first
116
+ )
117
+
118
+ @rebalance
119
+ end
120
+
121
+ def self.raw_rebalance?(raw_payment)
122
+ return false if raw_payment.htlcs.first.route.hops.size <= 2
123
+
124
+ destination_public_key = raw_payment.htlcs.first.route.hops.last.pub_key
125
+
126
+ Node.myself.public_key == destination_public_key
127
+ end
128
+
129
+ def self.raw_purpose(raw_payment)
130
+ return 'potential-submarine' if raw_potential_submarine?(raw_payment)
131
+ return 'rebalance' if raw_rebalance?(raw_payment)
132
+
133
+ 'payment'
134
+ end
135
+
136
+ def self.raw_potential_submarine?(raw_payment)
137
+ raw_payment.htlcs.first.route.hops.size == 1
138
+ end
139
+
140
+ def potential_submarine?
141
+ validated_htlcs_number!
142
+
143
+ @potential_submarine ||= Payment.raw_potential_submarine?(
144
+ @data[:list_payments][:payments].first
145
+ )
146
+ end
147
+
148
+ def validated_htlcs_number!
149
+ return unless @data[:list_payments][:payments].first.htlcs.size > 1
150
+
151
+ raise "Unexpected number of HTLCs (#{@data[:list_payments][:payments].first.htlcs.size}) for Payment"
152
+ end
153
+
154
+ def from
155
+ return @from if @from
156
+
157
+ if @hops
158
+ @from = @hops.first
159
+ return @from
160
+ end
161
+
162
+ validated_htlcs_number!
163
+
164
+ @from = PaymentChannel.new(
165
+ @data[:list_payments][:payments].first.htlcs.first.route.hops.first,
166
+ 1
167
+ )
168
+
169
+ @from
170
+ end
171
+
172
+ def to
173
+ return @to if @to
174
+
175
+ if @hops
176
+
177
+ @to = rebalance? ? @hops[@hops.size - 2] : @hops.last
178
+ return @to
179
+ end
180
+
181
+ validated_htlcs_number!
182
+
183
+ @to = if rebalance?
184
+ PaymentChannel.new(
185
+ @data[:list_payments][:payments].first.htlcs.first.route.hops[
186
+ @data[:list_payments][:payments].first.htlcs.first.route.hops.size - 2
187
+ ],
188
+ @data[:list_payments][:payments].first.htlcs.first.route.hops.size - 1
189
+ )
190
+ else
191
+ PaymentChannel.new(
192
+ @data[:list_payments][:payments].first.htlcs.first.route.hops.last,
193
+ @data[:list_payments][:payments].first.htlcs.first.route.hops.size
194
+ )
195
+ end
196
+
197
+ @to
198
+ end
199
+
200
+ def hops
201
+ return @hops if @hops
202
+
203
+ validated_htlcs_number!
204
+
205
+ @hops = @data[:list_payments][:payments].first.htlcs.first.route.hops.map.with_index do |raw_hop, i|
206
+ PaymentChannel.new(raw_hop, i + 1)
207
+ end
208
+ end
209
+
210
+ def preload_hops!
211
+ hops
212
+ true
213
+ end
214
+
215
+ def to_h
216
+ response = {
217
+ id: id,
218
+ hash: hash,
219
+ created_at: created_at,
220
+ purpose: purpose,
221
+ status: status,
222
+ amount: amount.to_h,
223
+ fee: {
224
+ milisatoshis: fee.milisatoshis,
225
+ parts_per_million: fee.parts_per_million(amount.milisatoshis)
226
+ }
227
+ }
228
+
229
+ if @respond_hops
230
+ preload_hops!
231
+ response[:from] = from.to_h
232
+ response[:to] = to.to_h
233
+ response[:hops] = hops.map(&:to_h)
234
+ else
235
+ response[:from] = from.to_h
236
+ response[:to] = to.to_h
237
+ end
238
+
239
+ response
240
+ end
241
+
242
+ def raw
243
+ {
244
+ list_payments: {
245
+ payments: [@data[:list_payments][:payments].first.to_h]
246
+ }
247
+ }
248
+ end
249
+ end
250
+ end
251
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../../components/lnd'
4
+
5
+ module Lighstorm
6
+ module Models
7
+ class Lightning
8
+ def initialize(platform, node)
9
+ raise 'cannot provide platform details for a node that is not yours' unless node.myself?
10
+
11
+ @platform = platform
12
+ end
13
+
14
+ def version
15
+ @version ||= @platform.data[:get_info].version
16
+ end
17
+
18
+ def raw
19
+ { get_info: @data[:get_info].to_h }
20
+ end
21
+
22
+ def to_h
23
+ {
24
+ implementation: 'lnd',
25
+ version: version
26
+ }
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../../components/lnd'
4
+
5
+ require_relative 'lightning'
6
+
7
+ module Lighstorm
8
+ module Models
9
+ class Platform
10
+ attr_reader :data
11
+
12
+ def initialize(node)
13
+ @node = node
14
+
15
+ response = Cache.for('lightning.get_info', ttl: 1) do
16
+ LND.instance.middleware('lightning.get_info') do
17
+ LND.instance.client.lightning.get_info
18
+ end
19
+ end
20
+
21
+ @data = { get_info: response }
22
+ end
23
+
24
+ def blockchain
25
+ @blockchain ||= @data[:get_info].chains.first.chain
26
+ end
27
+
28
+ def network
29
+ @network ||= @data[:get_info].chains.first.network
30
+ end
31
+
32
+ def lightning
33
+ @lightning ||= Lightning.new(self, @node)
34
+ end
35
+
36
+ def to_h
37
+ response = {
38
+ blockchain: blockchain,
39
+ network: network
40
+ }
41
+
42
+ response[:lightning] = lightning.to_h if @node.myself?
43
+
44
+ response
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../components/lnd'
4
+
5
+ require_relative '../edges/channel'
6
+
7
+ require_relative 'node/platform'
8
+
9
+ module Lighstorm
10
+ module Models
11
+ class Node
12
+ KIND = :node
13
+
14
+ attr_reader :alias, :public_key, :color
15
+
16
+ def self.myself
17
+ response = Cache.for('lightning.get_info', ttl: 1) do
18
+ LND.instance.middleware('lightning.get_info') do
19
+ LND.instance.client.lightning.get_info
20
+ end
21
+ end
22
+
23
+ Node.find_by_public_key(response.identity_pubkey, myself: true)
24
+ end
25
+
26
+ def self.find_by_public_key(public_key, myself: false)
27
+ Node.new({ public_key: public_key }, myself: myself)
28
+ end
29
+
30
+ def myself?
31
+ @myself
32
+ end
33
+
34
+ def platform
35
+ @platform ||= Platform.new(self)
36
+ end
37
+
38
+ def channels
39
+ raise 'cannot list channels from a node that is not yours' unless myself?
40
+
41
+ Channel.all
42
+ end
43
+
44
+ def raw
45
+ {
46
+ get_node_info: @data[:get_node_info].to_h
47
+ }
48
+ end
49
+
50
+ def to_h
51
+ {
52
+ alias: @alias,
53
+ public_key: @public_key,
54
+ color: @color,
55
+ platform: platform.to_h
56
+ }
57
+ end
58
+
59
+ private
60
+
61
+ def initialize(params, myself: false)
62
+ response = Cache.for(
63
+ 'lightning.get_node_info',
64
+ ttl: 5 * 60, params: { pub_key: params[:public_key] }
65
+ ) do
66
+ LND.instance.middleware('lightning.get_node_info') do
67
+ LND.instance.client.lightning.get_node_info(pub_key: params[:public_key])
68
+ end
69
+ end
70
+
71
+ @data = { get_node_info: response }
72
+
73
+ @myself = myself
74
+
75
+ @alias = @data[:get_node_info].node.alias
76
+ @public_key = @data[:get_node_info].node.pub_key
77
+ @color = @data[:get_node_info].node.color
78
+ end
79
+ end
80
+ end
81
+ end
data/models/rate.rb ADDED
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lighstorm
4
+ module Models
5
+ class Rate
6
+ # https://en.wikipedia.org/wiki/Parts-per_notation
7
+ def initialize(parts_per_million: nil)
8
+ raise 'missing parts_per_million' if parts_per_million.nil?
9
+
10
+ # TODO
11
+ @parts_per_million = parts_per_million
12
+ end
13
+
14
+ def to_h
15
+ {
16
+ parts_per_million: @parts_per_million
17
+ # satoshis: satoshis,
18
+ # bitcoins: bitcoins
19
+ }
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lighstorm
4
+ module Models
5
+ class Satoshis
6
+ def initialize(milisatoshis: nil)
7
+ raise 'missing milisatoshis' if milisatoshis.nil?
8
+
9
+ @amount_in_milisatoshis = milisatoshis
10
+ end
11
+
12
+ def parts_per_million(reference_milisatoshis)
13
+ (
14
+ (
15
+ if reference_milisatoshis.zero?
16
+ 0
17
+ else
18
+ @amount_in_milisatoshis.to_f /
19
+ reference_milisatoshis
20
+ end
21
+ ) * 1_000_000.0
22
+ )
23
+ end
24
+
25
+ def milisatoshis
26
+ @amount_in_milisatoshis
27
+ end
28
+
29
+ def satoshis
30
+ (@amount_in_milisatoshis.to_f / 1000.0).to_i
31
+ end
32
+
33
+ def bitcoins
34
+ @amount_in_milisatoshis.to_f / 100_000_000_000
35
+ end
36
+
37
+ def sats
38
+ satoshis
39
+ end
40
+
41
+ def msats
42
+ milisatoshis
43
+ end
44
+
45
+ def btc
46
+ bitcoins
47
+ end
48
+
49
+ def to_h
50
+ {
51
+ milisatoshis: milisatoshis
52
+ # satoshis: satoshis,
53
+ # bitcoins: bitcoins
54
+ }
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dotenv/load'
4
+
5
+ require_relative '../../static/spec'
6
+
7
+ require_relative '../../models/nodes/node'
8
+
9
+ require_relative '../../models/edges/channel'
10
+ require_relative '../../models/edges/forward'
11
+ require_relative '../../models/edges/payment'
12
+
13
+ module Lighstorm
14
+ Node = Models::Node
15
+ Channel = Models::Channel
16
+ Forward = Models::Forward
17
+ Payment = Models::Payment
18
+ Satoshis = Models::Satoshis
19
+
20
+ def self.config!(config)
21
+ LND.instance.config = config
22
+ end
23
+
24
+ def self.inject_middleware!(middleware_lambda)
25
+ LND.instance.middleware = middleware_lambda
26
+ end
27
+
28
+ def self.version
29
+ Static::SPEC[:version]
30
+ end
31
+ end
data/static/spec.rb ADDED
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lighstorm
4
+ module Static
5
+ SPEC = {
6
+ name: 'lighstorm',
7
+ version: '0.0.1',
8
+ author: 'icebaker',
9
+ summary: 'API for interacting with a Lightning Node.',
10
+ description: 'Lighstorm is an opinionated abstraction layer on top of the lnd-client for interacting with a Lightning Node.',
11
+ github: 'https://github.com/icebaker/lighstorm',
12
+ license: 'MIT'
13
+ }.freeze
14
+ end
15
+ end
metadata ADDED
@@ -0,0 +1,122 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lighstorm
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - icebaker
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-02-05 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: dotenv
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.8'
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 2.8.1
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: '2.8'
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 2.8.1
33
+ - !ruby/object:Gem::Dependency
34
+ name: lnd-client
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: 0.0.4
40
+ type: :runtime
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: 0.0.4
47
+ - !ruby/object:Gem::Dependency
48
+ name: zache
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: 0.12.0
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: 0.12.0
61
+ description: Lighstorm is an opinionated abstraction layer on top of the lnd-client
62
+ for interacting with a Lightning Node.
63
+ email:
64
+ executables: []
65
+ extensions: []
66
+ extra_rdoc_files: []
67
+ files:
68
+ - ".gitignore"
69
+ - ".rubocop.yml"
70
+ - Gemfile
71
+ - Gemfile.lock
72
+ - LICENSE
73
+ - README.md
74
+ - components/cache.rb
75
+ - components/lnd.rb
76
+ - lighstorm.gemspec
77
+ - models/connections/channel_node.rb
78
+ - models/connections/channel_node/accounting.rb
79
+ - models/connections/channel_node/constraints.rb
80
+ - models/connections/channel_node/fee.rb
81
+ - models/connections/channel_node/policy.rb
82
+ - models/connections/forward_channel.rb
83
+ - models/connections/payment_channel.rb
84
+ - models/edges/channel.rb
85
+ - models/edges/channel/accounting.rb
86
+ - models/edges/forward.rb
87
+ - models/edges/payment.rb
88
+ - models/nodes/node.rb
89
+ - models/nodes/node/lightning.rb
90
+ - models/nodes/node/platform.rb
91
+ - models/rate.rb
92
+ - models/satoshis.rb
93
+ - ports/dsl/lighstorm.rb
94
+ - static/spec.rb
95
+ homepage: https://github.com/icebaker/lighstorm
96
+ licenses:
97
+ - MIT
98
+ metadata:
99
+ allowed_push_host: https://rubygems.org
100
+ homepage_uri: https://github.com/icebaker/lighstorm
101
+ source_code_uri: https://github.com/icebaker/lighstorm
102
+ rubygems_mfa_required: 'true'
103
+ post_install_message:
104
+ rdoc_options: []
105
+ require_paths:
106
+ - ports/dsl
107
+ required_ruby_version: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ version: 3.0.0
112
+ required_rubygems_version: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: '0'
117
+ requirements: []
118
+ rubygems_version: 3.4.4
119
+ signing_key:
120
+ specification_version: 4
121
+ summary: API for interacting with a Lightning Node.
122
+ test_files: []