lighstorm 0.0.5 → 0.0.7
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +3 -3
- data/README.md +17 -55
- data/adapters/connections/channel_node/fee.rb +1 -1
- data/adapters/connections/channel_node/policy.rb +6 -6
- data/adapters/connections/channel_node.rb +1 -1
- data/adapters/connections/payment_channel.rb +2 -2
- data/adapters/edges/channel.rb +7 -7
- data/adapters/edges/forward.rb +3 -3
- data/adapters/edges/payment.rb +1 -1
- data/adapters/invoice.rb +16 -1
- data/adapters/payment_request.rb +14 -3
- data/components/cache.rb +8 -5
- data/controllers/action.rb +24 -0
- data/controllers/channel/actions/apply_gossip.rb +18 -18
- data/controllers/channel/actions/update_fee.rb +33 -22
- data/controllers/forward/group_by_channel.rb +3 -3
- data/controllers/invoice/actions/create.rb +39 -11
- data/controllers/invoice/actions/pay_through_route.rb +1 -1
- data/controllers/invoice.rb +3 -3
- data/docs/README.md +405 -140
- data/docs/_coverpage.md +6 -1
- data/docs/index.html +1 -1
- data/models/connections/channel_node/accounting.rb +1 -1
- data/models/connections/channel_node/fee.rb +5 -4
- data/models/connections/channel_node/htlc.rb +4 -4
- data/models/connections/forward_channel.rb +1 -1
- data/models/connections/payment_channel.rb +4 -4
- data/models/edges/channel/accounting.rb +5 -5
- data/models/edges/forward.rb +3 -3
- data/models/edges/groups/channel_forwards/analysis.rb +8 -8
- data/models/edges/payment.rb +3 -3
- data/models/errors.rb +1 -1
- data/models/payment_request.rb +2 -2
- data/models/satoshis.rb +12 -12
- data/static/cache.rb +2 -0
- data/static/spec.rb +1 -1
- metadata +3 -2
data/docs/README.md
CHANGED
@@ -6,9 +6,13 @@
|
|
6
6
|
|
7
7
|
_Lighstorm_ is an opinionated abstraction layer on top of the [lnd-client](https://github.com/icebaker/lnd-client).
|
8
8
|
|
9
|
-
It brings an [
|
9
|
+
It brings an [_object-oriented_](https://en.wikipedia.org/wiki/Object-oriented_programming) approach for interacting with a [Lightning Node](https://github.com/lightningnetwork/lnd), influenced by the [Active Record Pattern](https://www.martinfowler.com/eaaCatalog/activeRecord.html) and [Active Record Models](https://guides.rubyonrails.org/active_record_basics.html) conventions.
|
10
10
|
|
11
|
-
|
11
|
+
However, despite the fluidity of _Object Orientation_ being desired in its public interface, internally, most of its code is structured following the [_Hexagonal Architecture_](https://en.wikipedia.org/wiki/Hexagonal_architecture_(software)) and [_Functional Programming_](https://en.wikipedia.org/wiki/Functional_programming) principles.
|
12
|
+
|
13
|
+
It aims to be intuitive to use while being highly **reliable**, as it deals with people's money, and easily testable since its [tests](?id=testing) are the foundation for its reliability.
|
14
|
+
|
15
|
+
Although it tries to stay close to [Lightning's terminologies](https://docs.lightning.engineering/the-lightning-network/overview), it brings its own vocabulary and [data modeling](?id=data-modeling), optimizing for [programmer happiness](https://rubyonrails.org/doctrine#optimize-for-programmer-happiness).
|
12
16
|
|
13
17
|
# Getting Started
|
14
18
|
|
@@ -23,7 +27,7 @@ Lighstorm::Channel.mine.first.myself.node.alias
|
|
23
27
|
Add to your `Gemfile`:
|
24
28
|
|
25
29
|
```ruby
|
26
|
-
gem 'lighstorm', '~> 0.0.
|
30
|
+
gem 'lighstorm', '~> 0.0.7'
|
27
31
|
```
|
28
32
|
|
29
33
|
Run `bundle install`.
|
@@ -56,11 +60,11 @@ Lighstorm.config!(
|
|
56
60
|
```ruby
|
57
61
|
require 'lighstorm'
|
58
62
|
|
59
|
-
puts Lighstorm.version # => 0.0.
|
63
|
+
puts Lighstorm.version # => 0.0.7
|
60
64
|
|
61
|
-
Lighstorm::
|
62
|
-
|
63
|
-
)
|
65
|
+
Lighstorm::Invoice.create(
|
66
|
+
description: 'Coffee', millisatoshis: 1000
|
67
|
+
)
|
64
68
|
|
65
69
|
Lighstorm::Node.myself.alias # => icebaker/old-stone
|
66
70
|
Lighstorm::Node.myself.public_key # => 02d3...e997
|
@@ -73,7 +77,7 @@ Lighstorm::Channel.mine.first.partner.node.alias
|
|
73
77
|
|
74
78
|
forward = Lighstorm::Forward.all(limit: 10).first
|
75
79
|
|
76
|
-
forward.in.amount.
|
80
|
+
forward.in.amount.millisatoshis # => 75621650
|
77
81
|
forward.in.amount.satoshis # => 75621
|
78
82
|
forward.in.amount.bitcoins # => 0.0007562165
|
79
83
|
forward.in.channel.partner.node.alias
|
@@ -88,6 +92,10 @@ payment.to.channel.id # => 821539695188246532
|
|
88
92
|
payment.amount.sats # => 957262
|
89
93
|
payment.hops.size # => 4
|
90
94
|
payment.hops.first.channel.partner.node.alias
|
95
|
+
|
96
|
+
Lighstorm::Satoshis.new(
|
97
|
+
millisatoshis: 75621650
|
98
|
+
).satoshis # => 75621
|
91
99
|
```
|
92
100
|
|
93
101
|
# Data Modeling
|
@@ -105,20 +113,20 @@ So, we are going to think in terms of _Edges_, _Nodes_, and _Connections_:
|
|
105
113
|
</a>
|
106
114
|
</center>
|
107
115
|
|
108
|
-
|
116
|
+
### Channel
|
109
117
|
|
110
118
|
```ruby
|
111
119
|
channel = Lighstorm::Channel.mine.first
|
112
120
|
|
113
121
|
channel.id
|
114
122
|
|
115
|
-
channel.accounting.capacity.
|
123
|
+
channel.accounting.capacity.millisatoshis
|
116
124
|
|
117
|
-
channel.partner.accounting.balance.
|
125
|
+
channel.partner.accounting.balance.millisatoshis
|
118
126
|
channel.partner.node.alias
|
119
127
|
channel.partner.policy.fee.rate.parts_per_million
|
120
128
|
|
121
|
-
channel.myself.accounting.balance.
|
129
|
+
channel.myself.accounting.balance.millisatoshis
|
122
130
|
channel.myself.node.alias
|
123
131
|
channel.myself.policy.fee.rate.parts_per_million
|
124
132
|
```
|
@@ -130,18 +138,18 @@ channel.myself.policy.fee.rate.parts_per_million
|
|
130
138
|
</a>
|
131
139
|
</center>
|
132
140
|
|
133
|
-
|
141
|
+
### Forward
|
134
142
|
|
135
143
|
```ruby
|
136
144
|
forward = Lighstorm::Forward.last
|
137
145
|
|
138
146
|
forward.at
|
139
147
|
|
140
|
-
forward.fee.
|
148
|
+
forward.fee.millisatoshis
|
141
149
|
forward.fee.parts_per_million
|
142
150
|
|
143
|
-
forward.in.amount.
|
144
|
-
forward.out.amount.
|
151
|
+
forward.in.amount.millisatoshis
|
152
|
+
forward.out.amount.millisatoshis
|
145
153
|
|
146
154
|
forward.in.channel.id
|
147
155
|
forward.in.channel.partner.node.alias
|
@@ -157,7 +165,7 @@ forward.out.channel.partner.node.alias
|
|
157
165
|
</a>
|
158
166
|
</center>
|
159
167
|
|
160
|
-
|
168
|
+
### Payment
|
161
169
|
|
162
170
|
```ruby
|
163
171
|
payment = Payment.last
|
@@ -168,24 +176,24 @@ payment.created_at
|
|
168
176
|
# https://github.com/lightning/bolts/blob/master/11-payment-encoding.md
|
169
177
|
payment.request.code # "lnbc20m1pv...qqdhhwkj"
|
170
178
|
|
171
|
-
payment.request.amount.
|
179
|
+
payment.request.amount.millisatoshis
|
172
180
|
|
173
181
|
payment.from.hop
|
174
|
-
payment.from.amount.
|
175
|
-
payment.from.fee.
|
182
|
+
payment.from.amount.millisatoshis
|
183
|
+
payment.from.fee.millisatoshis
|
176
184
|
payment.from.channel.id
|
177
185
|
payment.from.channel.target.alias
|
178
186
|
payment.from.channel.exit.alias
|
179
187
|
|
180
188
|
payment.to.hop
|
181
|
-
payment.to.amount.
|
182
|
-
payment.to.fee.
|
189
|
+
payment.to.amount.millisatoshis
|
190
|
+
payment.to.fee.millisatoshis
|
183
191
|
payment.to.channel.id
|
184
192
|
payment.to.channel.target.alias
|
185
193
|
|
186
194
|
payment.hops[0].hop
|
187
|
-
payment.hops[0].amount.
|
188
|
-
payment.hops[0].fee.
|
195
|
+
payment.hops[0].amount.millisatoshis
|
196
|
+
payment.hops[0].fee.millisatoshis
|
189
197
|
payment.hops[0].channel.id
|
190
198
|
payment.hops[0].channel.target.alias
|
191
199
|
```
|
@@ -197,80 +205,6 @@ payment.hops[0].channel.target.alias
|
|
197
205
|
</a>
|
198
206
|
</center>
|
199
207
|
|
200
|
-
# Error Handling
|
201
|
-
|
202
|
-
## Rescuing
|
203
|
-
```ruby
|
204
|
-
require 'lighstorm'
|
205
|
-
|
206
|
-
channel = Lighstorm::Channel.mine.first
|
207
|
-
|
208
|
-
begin
|
209
|
-
channel.myself.policy.fee.update(
|
210
|
-
{ rate: { parts_per_million: -1 } }, preview: true
|
211
|
-
)
|
212
|
-
rescue Lighstorm::Errors::NegativeNotAllowedError => error
|
213
|
-
puts error.message # 'fee rate can't be negative: -1'
|
214
|
-
end
|
215
|
-
|
216
|
-
begin
|
217
|
-
channel.myself.policy.fee.update(
|
218
|
-
{ rate: { parts_per_million: -1 } }, preview: true
|
219
|
-
)
|
220
|
-
rescue Lighstorm::Errors::LighstormError => error
|
221
|
-
puts error.message # 'fee rate can't be negative: -1'
|
222
|
-
end
|
223
|
-
```
|
224
|
-
|
225
|
-
### For Short
|
226
|
-
|
227
|
-
```ruby
|
228
|
-
require 'lighstorm'
|
229
|
-
require 'lighstorm/errors'
|
230
|
-
|
231
|
-
channel = Lighstorm::Channel.mine.first
|
232
|
-
|
233
|
-
begin
|
234
|
-
channel.myself.policy.fee.update(
|
235
|
-
{ rate: { parts_per_million: -1 } }, preview: true
|
236
|
-
)
|
237
|
-
rescue NegativeNotAllowedError => error
|
238
|
-
puts error.message # "fee rate can't be negative: -1"
|
239
|
-
end
|
240
|
-
|
241
|
-
begin
|
242
|
-
channel.myself.policy.fee.update(
|
243
|
-
{ rate: { parts_per_million: -1 } }, preview: true
|
244
|
-
)
|
245
|
-
rescue LighstormError => error
|
246
|
-
puts error.message # "fee rate can't be negative: -1"
|
247
|
-
end
|
248
|
-
```
|
249
|
-
|
250
|
-
## Errors
|
251
|
-
```ruby
|
252
|
-
LighstormError
|
253
|
-
|
254
|
-
IncoherentGossipError
|
255
|
-
|
256
|
-
TooManyArgumentsError
|
257
|
-
MissingCredentialsError
|
258
|
-
MissingGossipHandlerError
|
259
|
-
MissingMilisatoshisError
|
260
|
-
MissingPartsPerMillionError
|
261
|
-
MissingTTLError
|
262
|
-
|
263
|
-
NegativeNotAllowedError
|
264
|
-
|
265
|
-
NotYourChannelError
|
266
|
-
NotYourNodeError
|
267
|
-
UnknownChannelError
|
268
|
-
|
269
|
-
OperationNotAllowedError
|
270
|
-
UnexpectedNumberOfHTLCsError
|
271
|
-
UpdateChannelPolicyError
|
272
|
-
```
|
273
|
-
|
274
208
|
# API
|
275
209
|
|
276
210
|
## Node
|
@@ -338,10 +272,10 @@ channel.state
|
|
338
272
|
channel.active?
|
339
273
|
channel.exposure
|
340
274
|
|
341
|
-
channel.accounting.capacity.
|
342
|
-
channel.accounting.sent.
|
343
|
-
channel.accounting.received.
|
344
|
-
channel.accounting.unsettled.
|
275
|
+
channel.accounting.capacity.millisatoshis
|
276
|
+
channel.accounting.sent.millisatoshis
|
277
|
+
channel.accounting.received.millisatoshis
|
278
|
+
channel.accounting.unsettled.millisatoshis
|
345
279
|
|
346
280
|
# Channels that don't belong to you:
|
347
281
|
channel.partners
|
@@ -364,13 +298,13 @@ channel.partner.node.public_key
|
|
364
298
|
channel.partner.node.alias
|
365
299
|
channel.partner.node.color
|
366
300
|
|
367
|
-
channel.partner.accounting.balance.
|
301
|
+
channel.partner.accounting.balance.millisatoshis
|
368
302
|
|
369
|
-
channel.partner.policy.fee.base.
|
303
|
+
channel.partner.policy.fee.base.millisatoshis
|
370
304
|
channel.partner.policy.fee.rate.parts_per_million
|
371
305
|
|
372
|
-
channel.partner.policy.htlc.minimum.
|
373
|
-
channel.partner.policy.htlc.maximum.
|
306
|
+
channel.partner.policy.htlc.minimum.millisatoshis
|
307
|
+
channel.partner.policy.htlc.maximum.millisatoshis
|
374
308
|
channel.partner.policy.htlc.blocks.delta.minimum
|
375
309
|
|
376
310
|
channel.myself
|
@@ -381,27 +315,29 @@ channel.myself.node.public_key
|
|
381
315
|
channel.myself.node.alias
|
382
316
|
channel.myself.node.color
|
383
317
|
|
384
|
-
channel.myself.accounting.balance.
|
318
|
+
channel.myself.accounting.balance.millisatoshis
|
385
319
|
|
386
|
-
channel.myself.policy.fee.base.
|
320
|
+
channel.myself.policy.fee.base.millisatoshis
|
387
321
|
channel.myself.policy.fee.rate.parts_per_million
|
388
322
|
|
389
|
-
channel.myself.policy.htlc.minimum.
|
390
|
-
channel.myself.policy.htlc.maximum.
|
323
|
+
channel.myself.policy.htlc.minimum.millisatoshis
|
324
|
+
channel.myself.policy.htlc.maximum.millisatoshis
|
391
325
|
channel.myself.policy.htlc.blocks.delta.minimum
|
392
326
|
```
|
393
327
|
|
394
|
-
###
|
328
|
+
### Fee Update
|
395
329
|
|
396
330
|
```ruby
|
397
331
|
channel = Lighstorm::Channel.mine.first
|
398
332
|
|
333
|
+
# 'preview' let you check the expected operation
|
334
|
+
# before actually performing it for debug purposes
|
399
335
|
channel.myself.policy.fee.update(
|
400
336
|
{ rate: { parts_per_million: 25 } }, preview: true
|
401
337
|
)
|
402
338
|
|
403
339
|
channel.myself.policy.fee.update(
|
404
|
-
{ base: {
|
340
|
+
{ base: { millisatoshis: 1 } }
|
405
341
|
)
|
406
342
|
|
407
343
|
channel.myself.policy.fee.update(
|
@@ -409,12 +345,14 @@ channel.myself.policy.fee.update(
|
|
409
345
|
)
|
410
346
|
|
411
347
|
channel.myself.policy.fee.update(
|
412
|
-
{ base: {
|
348
|
+
{ base: { millisatoshis: 1 }, rate: { parts_per_million: 25 } }
|
413
349
|
)
|
414
350
|
```
|
415
351
|
|
416
352
|
## Invoice
|
417
353
|
|
354
|
+
[Understanding Lightning Invoices](https://docs.lightning.engineering/the-lightning-network/payment-lifecycle/understanding-lightning-invoices)
|
355
|
+
|
418
356
|
```ruby
|
419
357
|
Lighstorm::Invoice
|
420
358
|
Lighstorm::Invoice.all
|
@@ -440,7 +378,7 @@ invoice.state
|
|
440
378
|
# https://github.com/lightning/bolts/blob/master/11-payment-encoding.md
|
441
379
|
invoice.request.code # "lnbc20m1pv...qqdhhwkj"
|
442
380
|
|
443
|
-
invoice.request.amount.
|
381
|
+
invoice.request.amount.millisatoshis
|
444
382
|
|
445
383
|
invoice.request.description.memo
|
446
384
|
invoice.request.description.hash
|
@@ -452,6 +390,27 @@ invoice.request.secret.hash
|
|
452
390
|
invoice.request.address
|
453
391
|
```
|
454
392
|
|
393
|
+
### Create
|
394
|
+
|
395
|
+
[Understanding Lightning Invoices](https://docs.lightning.engineering/the-lightning-network/payment-lifecycle/understanding-lightning-invoices)
|
396
|
+
|
397
|
+
```ruby
|
398
|
+
# 'preview' let you check the expected operation
|
399
|
+
# before actually performing it for debug purposes
|
400
|
+
preview = Lighstorm::Invoice.create(
|
401
|
+
description: 'Coffee', millisatoshis: 1000, preview: true
|
402
|
+
)
|
403
|
+
|
404
|
+
action = Lighstorm::Invoice.create(
|
405
|
+
description: 'Piña Colada', millisatoshis: 1000
|
406
|
+
)
|
407
|
+
|
408
|
+
action.to_h
|
409
|
+
|
410
|
+
action.response
|
411
|
+
invoice = action.result
|
412
|
+
```
|
413
|
+
|
455
414
|
## Payment
|
456
415
|
|
457
416
|
[![This is an image representing Payment as a graph.](https://raw.githubusercontent.com/icebaker/assets/main/lighstorm/graph-payment.png)](https://raw.githubusercontent.com/icebaker/assets/main/lighstorm/graph-payment.png)
|
@@ -484,15 +443,15 @@ payment.created_at
|
|
484
443
|
payment.settled_at
|
485
444
|
payment.purpose
|
486
445
|
|
487
|
-
payment.fee.
|
446
|
+
payment.fee.millisatoshis
|
488
447
|
payment.fee.parts_per_million(
|
489
|
-
payment.request.amount.
|
448
|
+
payment.request.amount.millisatoshis
|
490
449
|
)
|
491
450
|
|
492
451
|
# https://github.com/lightning/bolts/blob/master/11-payment-encoding.md
|
493
452
|
payment.request.code # "lnbc20m1pv...qqdhhwkj"
|
494
453
|
|
495
|
-
payment.request.amount.
|
454
|
+
payment.request.amount.millisatoshis
|
496
455
|
|
497
456
|
# https://docs.lightning.engineering/the-lightning-network/multihop-payments
|
498
457
|
payment.request.secret.preimage
|
@@ -504,9 +463,9 @@ payment.request.description.memo
|
|
504
463
|
payment.request.description.hash
|
505
464
|
|
506
465
|
payment.from.hop
|
507
|
-
payment.from.amount.
|
508
|
-
payment.from.fee.
|
509
|
-
payment.from.fee.parts_per_million(payment.from.amount.
|
466
|
+
payment.from.amount.millisatoshis
|
467
|
+
payment.from.fee.millisatoshis
|
468
|
+
payment.from.fee.parts_per_million(payment.from.amount.millisatoshis)
|
510
469
|
|
511
470
|
payment.from.channel.id
|
512
471
|
|
@@ -519,9 +478,9 @@ payment.from.channel.exit.alias
|
|
519
478
|
payment.from.channel.exit.color
|
520
479
|
|
521
480
|
payment.to.hop
|
522
|
-
payment.to.amount.
|
523
|
-
payment.to.fee.
|
524
|
-
payment.to.fee.parts_per_million(payment.to.amount.
|
481
|
+
payment.to.amount.millisatoshis
|
482
|
+
payment.to.fee.millisatoshis
|
483
|
+
payment.to.fee.parts_per_million(payment.to.amount.millisatoshis)
|
525
484
|
|
526
485
|
payment.to.channel.id
|
527
486
|
|
@@ -539,9 +498,9 @@ payment.hops[0].first?
|
|
539
498
|
payment.hops[0].last?
|
540
499
|
|
541
500
|
payment.hops[0].hop
|
542
|
-
payment.hops[0].amount.
|
543
|
-
payment.hops[0].fee.
|
544
|
-
payment.hops[0].fee.parts_per_million(payment.hops[0].amount.
|
501
|
+
payment.hops[0].amount.millisatoshis
|
502
|
+
payment.hops[0].fee.millisatoshis
|
503
|
+
payment.hops[0].fee.parts_per_million(payment.hops[0].amount.millisatoshis)
|
545
504
|
|
546
505
|
payment.hops[0].channel.id
|
547
506
|
|
@@ -591,19 +550,19 @@ forward._key
|
|
591
550
|
|
592
551
|
forward.at
|
593
552
|
|
594
|
-
forward.fee.
|
553
|
+
forward.fee.millisatoshis
|
595
554
|
forward.fee.parts_per_million(
|
596
|
-
forward.in.amount.
|
555
|
+
forward.in.amount.millisatoshis
|
597
556
|
)
|
598
557
|
|
599
|
-
forward.in.amount.
|
558
|
+
forward.in.amount.millisatoshis
|
600
559
|
|
601
560
|
forward.in.channel.id
|
602
561
|
forward.in.channel.partner.node.alias
|
603
562
|
forward.in.channel.partner.node.public_key
|
604
563
|
forward.in.channel.partner.node.color
|
605
564
|
|
606
|
-
forward.out.amount.
|
565
|
+
forward.out.amount.millisatoshis
|
607
566
|
|
608
567
|
forward.out.channel.id
|
609
568
|
forward.out.channel.partner.node.alias
|
@@ -626,12 +585,12 @@ group._key
|
|
626
585
|
|
627
586
|
group.last_at
|
628
587
|
group.analysis.count
|
629
|
-
group.analysis.sums.amount.
|
630
|
-
group.analysis.sums.fee.
|
631
|
-
group.analysis.averages.amount.
|
632
|
-
group.analysis.averages.fee.
|
588
|
+
group.analysis.sums.amount.millisatoshis
|
589
|
+
group.analysis.sums.fee.millisatoshis
|
590
|
+
group.analysis.averages.amount.millisatoshis
|
591
|
+
group.analysis.averages.fee.millisatoshis
|
633
592
|
group.analysis.averages.fee.parts_per_million(
|
634
|
-
group.analysis.averages.amount.
|
593
|
+
group.analysis.averages.amount.millisatoshis
|
635
594
|
)
|
636
595
|
|
637
596
|
group.channel.id
|
@@ -705,11 +664,11 @@ Lighstorm::Channel.adapt(dump: channel.dump)
|
|
705
664
|
|
706
665
|
```ruby
|
707
666
|
Lighstorm::Satoshis
|
708
|
-
Lighstorm::Satoshis.new(
|
667
|
+
Lighstorm::Satoshis.new(millisatoshis: 75621650)
|
709
668
|
|
710
669
|
satoshis.to_h
|
711
670
|
|
712
|
-
satoshis.
|
671
|
+
satoshis.millisatoshis
|
713
672
|
satoshis.satoshis
|
714
673
|
satoshis.bitcoins
|
715
674
|
|
@@ -717,13 +676,319 @@ satoshis.msats
|
|
717
676
|
satoshis.sats
|
718
677
|
satoshis.btc
|
719
678
|
|
720
|
-
|
721
|
-
satoshis.parts_per_million(
|
679
|
+
reference_in_millisatoshis = 75621650000
|
680
|
+
satoshis.parts_per_million(reference_in_millisatoshis)
|
681
|
+
```
|
682
|
+
|
683
|
+
# Error Handling
|
684
|
+
|
685
|
+
## Rescuing
|
686
|
+
```ruby
|
687
|
+
require 'lighstorm'
|
688
|
+
|
689
|
+
channel = Lighstorm::Channel.mine.first
|
690
|
+
|
691
|
+
begin
|
692
|
+
channel.myself.policy.fee.update(
|
693
|
+
{ rate: { parts_per_million: -1 } }, preview: true
|
694
|
+
)
|
695
|
+
rescue Lighstorm::Errors::NegativeNotAllowedError => error
|
696
|
+
puts error.message # 'fee rate can't be negative: -1'
|
697
|
+
end
|
698
|
+
|
699
|
+
begin
|
700
|
+
channel.myself.policy.fee.update(
|
701
|
+
{ rate: { parts_per_million: -1 } }, preview: true
|
702
|
+
)
|
703
|
+
rescue Lighstorm::Errors::LighstormError => error
|
704
|
+
puts error.message # 'fee rate can't be negative: -1'
|
705
|
+
end
|
722
706
|
```
|
707
|
+
|
708
|
+
### For Short
|
709
|
+
|
710
|
+
```ruby
|
711
|
+
require 'lighstorm'
|
712
|
+
require 'lighstorm/errors'
|
713
|
+
|
714
|
+
channel = Lighstorm::Channel.mine.first
|
715
|
+
|
716
|
+
begin
|
717
|
+
channel.myself.policy.fee.update(
|
718
|
+
{ rate: { parts_per_million: -1 } }, preview: true
|
719
|
+
)
|
720
|
+
rescue NegativeNotAllowedError => error
|
721
|
+
puts error.message # "fee rate can't be negative: -1"
|
722
|
+
end
|
723
|
+
|
724
|
+
begin
|
725
|
+
channel.myself.policy.fee.update(
|
726
|
+
{ rate: { parts_per_million: -1 } }, preview: true
|
727
|
+
)
|
728
|
+
rescue LighstormError => error
|
729
|
+
puts error.message # "fee rate can't be negative: -1"
|
730
|
+
end
|
731
|
+
```
|
732
|
+
|
733
|
+
## Errors
|
734
|
+
```ruby
|
735
|
+
LighstormError
|
736
|
+
|
737
|
+
IncoherentGossipError
|
738
|
+
|
739
|
+
TooManyArgumentsError
|
740
|
+
MissingCredentialsError
|
741
|
+
MissingGossipHandlerError
|
742
|
+
MissingMillisatoshisError
|
743
|
+
MissingPartsPerMillionError
|
744
|
+
MissingTTLError
|
745
|
+
|
746
|
+
NegativeNotAllowedError
|
747
|
+
|
748
|
+
NotYourChannelError
|
749
|
+
NotYourNodeError
|
750
|
+
UnknownChannelError
|
751
|
+
|
752
|
+
OperationNotAllowedError
|
753
|
+
UnexpectedNumberOfHTLCsError
|
754
|
+
UpdateChannelPolicyError
|
755
|
+
```
|
756
|
+
|
757
|
+
# Development
|
758
|
+
|
759
|
+
Copy the `.env.example` file to `.env` and provide the required data.
|
760
|
+
|
761
|
+
```ruby
|
762
|
+
# Gemfile
|
763
|
+
gem 'lighstorm', path: '/home/user/lighstorm'
|
764
|
+
|
765
|
+
# demo.rb
|
766
|
+
require 'lighstorm'
|
767
|
+
|
768
|
+
puts Lighstorm.version # => 0.0.7
|
769
|
+
```
|
770
|
+
|
771
|
+
```sh
|
772
|
+
bundle
|
773
|
+
rubocop -A
|
774
|
+
```
|
775
|
+
|
776
|
+
## Testing
|
777
|
+
|
778
|
+
Copy the `.env.example` file to `.env` and provide the required data.
|
779
|
+
|
780
|
+
```
|
781
|
+
bundle
|
782
|
+
|
783
|
+
bundle exec rspec
|
784
|
+
```
|
785
|
+
### Approach
|
786
|
+
|
787
|
+
Writing tests for software that indirectly performs [blockchain](https://en.wikipedia.org/wiki/Blockchain) operations, relies on an external [gRPC](https://grpc.io) API, and is highly influenced by volatile and uncertain [states](https://en.wikipedia.org/wiki/State_(computer_science)) can be [challenging](https://www.youtube.com/watch?v=lKXe3HUG2l4).
|
788
|
+
|
789
|
+
While aiming for the _look and feel_ of _[Object Orientation](https://en.wikipedia.org/wiki/Object-oriented_programming)_, I don't want a mesh of objects with volatile states that can change at any time. I'm not saying it wouldn't be possible to make it reliable and easily testable this way, but I choose to follow a [_Functional Programming_](https://en.wikipedia.org/wiki/Functional_programming) approach internally, as my knowledge and experience can provide greater reliability for the software this way.
|
790
|
+
|
791
|
+
The core idea is to separate **data** from **behavior** as much as possible. So, a _Model_ internally is just a dummy wrapper that _models_ the data to provide a fluid experience. Alongside that, I make things as [small as possible](https://www.youtube.com/watch?v=8bZh5LMaSmE) and extract desired results from composing them.
|
792
|
+
|
793
|
+
Let's get practical.
|
794
|
+
|
795
|
+
#### Requesting
|
796
|
+
|
797
|
+
To _request_ all Nodes you:
|
798
|
+
```ruby
|
799
|
+
Lighstorm::Node.all
|
800
|
+
```
|
801
|
+
|
802
|
+
Internally, what's happening:
|
803
|
+
```ruby
|
804
|
+
nodes = Lighstorm::Node.all
|
805
|
+
|
806
|
+
data = Controllers::Node::All.fetch # side effect
|
807
|
+
adapted = Controllers::Node::All.adapt(data) # pure
|
808
|
+
transformed = Controllers::Node::All.transform(adapted) # pure
|
809
|
+
models = Controllers::Node::All.model(transformed) # pure
|
810
|
+
nodes = models # pure
|
811
|
+
|
812
|
+
nodes.first.public_key
|
813
|
+
```
|
814
|
+
|
815
|
+
So, `fetch` -> `adapt` -> `transform` -> `model`:
|
816
|
+
|
817
|
+
![A diagram illustrating the Request Process described above.](https://raw.githubusercontent.com/icebaker/assets/main/lighstorm/request.png)
|
818
|
+
|
819
|
+
The advantage of this approach is that only `fetch` may generate [side effects](https://en.wikipedia.org/wiki/Side_effect_(computer_science)). All other methods ( `adapt`, `transform`, `model`) are [pure functions](https://en.wikipedia.org/wiki/Pure_function). This means that most of the code is easily testable and reliable, and `fetch` is the only thing we need [mock](https://en.wikipedia.org/wiki/Mock_object) in tests and worry about possible side effects.
|
820
|
+
|
821
|
+
The downside is that we can't [lazy-load](https://en.wikipedia.org/wiki/Lazy_loading) data, as we must know what data we will need beforehand.
|
822
|
+
|
823
|
+
#### Performing Actions
|
824
|
+
|
825
|
+
To perform an _action_, like creating an Invoice, you:
|
826
|
+
```ruby
|
827
|
+
Lighstorm::Invoice.create(
|
828
|
+
description: 'Coffee', millisatoshis: 1000
|
829
|
+
)
|
830
|
+
```
|
831
|
+
|
832
|
+
Internally, what's happening:
|
833
|
+
```ruby
|
834
|
+
action = Lighstorm::Invoice.create(description: 'Coffee', millisatoshis: 1000)
|
835
|
+
|
836
|
+
request = Controllers::Invoice::Create.prepare(params) # pure
|
837
|
+
response = Controllers::Invoice::Create.dispatch(request) # side effect
|
838
|
+
adapted = Controllers::Invoice::Create.adapt(response) # pure
|
839
|
+
data = Controllers::Invoice::Create.fetch(adapted) # side effect
|
840
|
+
model = Controllers::Invoice::Create.model(data) # pure
|
841
|
+
action = { response: response, result: model } # pure
|
842
|
+
|
843
|
+
invoice = action.result
|
844
|
+
```
|
845
|
+
|
846
|
+
So, `prepare` -> `dispatch` -> `adapt` -> `fetch` -> `model`:
|
847
|
+
|
848
|
+
![A diagram illustrating the Action Process described above.](https://raw.githubusercontent.com/icebaker/assets/main/lighstorm/action.png)
|
849
|
+
|
850
|
+
The advantage of this approach is that only `dispatch` and `fetch` may generate [side effects](https://en.wikipedia.org/wiki/Side_effect_(computer_science)). All other methods ( `prepare`, `adapt`, `model`) are [pure functions](https://en.wikipedia.org/wiki/Pure_function). This means that most of the code is easily testable and reliable. `dispatch` and `fetch` are the only things we need [mock](https://en.wikipedia.org/wiki/Mock_object) in tests and worry about possible side effects.
|
851
|
+
|
852
|
+
### VCR
|
853
|
+
|
854
|
+
After understanding the [approach](?id=approach), we need to make [mocking](https://en.wikipedia.org/wiki/Mock_object) as straightforward as possible for a painless test writing experience, which leads to lots of tests being written, improving the reliability of the code.
|
855
|
+
|
856
|
+
_Mock_ is not precisely the approach we are going to use. Instead, we're going to make side effects easily reproducible. The internal [VCR](https://github.com/icebaker/lighstorm/blob/main/spec/helpers/vcr.rb) helper - _whose name is influenced by the [vcr](https://github.com/vcr/vcr) project but has an entirely different implementation_ - provides two kinds of reproducible recordings:
|
857
|
+
|
858
|
+
**Tape:**
|
859
|
+
|
860
|
+
It is a kind of recording that is easily generated, leading you to frequently delete them and recreate new ones without concerns. You will likely use it for [_requests_](?id=requesting), like fetching Node data. It remembers a real-life [Tape](https://en.wikipedia.org/wiki/Cassette_tape) that is easy to record with a [VCR](https://en.wikipedia.org/wiki/Videocassette_recorder).
|
861
|
+
|
862
|
+
**Reel:**
|
863
|
+
|
864
|
+
It is a kind of recording that is not so easily generated, leading you to avoid generating them as much as possible. You will likely use it for [_actions_](?id=performing-actions) like paying an Invoice. It remembers a real-life [Film Reel](https://en.wikipedia.org/wiki/Film_stock) that requires a lot of work to [process](https://en.wikipedia.org/wiki/Photographic_processing).
|
865
|
+
|
866
|
+
#### Replaying
|
867
|
+
|
868
|
+
To create a replayable _Tape_:
|
869
|
+
|
870
|
+
```ruby
|
871
|
+
public_key = '02d3c80335a8ccb2ed364c06875f32240f36f7edb37d80f8dbe321b4c364b6e997'
|
872
|
+
|
873
|
+
data = VCR.tape.replay('lightning.get_node_info', pub_key: public_key) do
|
874
|
+
Lighstorm::Ports::GRPC.lightning.get_node_info(pub_key: public_key).to_h
|
875
|
+
end
|
876
|
+
```
|
877
|
+
|
878
|
+
It will generate a `.bin` file inside `spec/data/tapes/` containing the data [_marshaled_](https://ruby-doc.org/core-3.0.0/Marshal.html).
|
879
|
+
|
880
|
+
If you want to force the overwrite of a Tape, replace `replay` with `replay!` Remember to undo it afterward, replacing `replay!` with `replay`.
|
881
|
+
|
882
|
+
To create a replayable _Reel_, just do all the same, but replace `VCR.tape` with `VCR.reel`:
|
883
|
+
|
884
|
+
```ruby
|
885
|
+
response = VCR.reel.replay(
|
886
|
+
'lightning.add_invoice',
|
887
|
+
memo: 'Coffee', value_msat: 1000
|
888
|
+
) do
|
889
|
+
Lighstorm::Ports::GRPC.lightning.add_invoice(
|
890
|
+
memo: 'Coffee', value_msat: 1000
|
891
|
+
).to_h
|
892
|
+
end
|
893
|
+
```
|
894
|
+
|
895
|
+
By understanding its basic operation, you can become creative using [Proc](https://ruby-doc.org/core-3.0.0/Proc.html). Search the code for [`VCR.tape.replay`](https://github.com/icebaker/lighstorm/search?q=VCR.tape.replay&type=code) or [`VCR.reel.replay`](https://github.com/icebaker/lighstorm/search?q=VCR.reel.replay&type=code) to understand its practical use.
|
896
|
+
|
897
|
+
#### Security
|
898
|
+
|
899
|
+
Ideally, you will write and run your tests over some Bitcoin [Testnet](https://en.wikipedia.org/wiki/Testnet). There are tools for helping you build a Test Environment like [_Polar_](https://lightningpolar.com).
|
900
|
+
|
901
|
+
Regardless, all _Tapes_ and _Reels_ undergo a [sanitization](https://github.com/icebaker/lighstorm/blob/main/spec/helpers/sanitizer_spec.rb) process before being recorded to remove potentially dangerous (like `payment_preimage`) or privacy-exposing (like `payment_addr`) data. All data potentially [unsafe](https://github.com/icebaker/lighstorm/blob/main/spec/helpers/sanitizer/unsafe.rb) is replaced by randomly generated data of equivalent type and size. If unknown data emerges, it needs to be classified as [safe](https://github.com/icebaker/lighstorm/blob/main/spec/helpers/sanitizer/safe.rb) or [unsafe](https://github.com/icebaker/lighstorm/blob/main/spec/helpers/sanitizer/unsafe.rb). Otherwise, it will be impossible to be recorded, generating an [error](https://github.com/icebaker/lighstorm/blob/main/spec/helpers/sanitizer.rb#L102).
|
902
|
+
|
903
|
+
### Contracts
|
904
|
+
|
905
|
+
Sometimes you are interested in testing the _shape_ of something instead of its _content_. Symptoms of this appear when you change something in your code and have to mechanically fix dozens of tests because of a minor thing that doesn't make a real difference to what you are testing. This may lead you to avoid doing some important changes because it will be too boring to fix the tests.
|
906
|
+
|
907
|
+
It usually plays like this:
|
908
|
+
|
909
|
+
```ruby
|
910
|
+
expect(something.to_h).to eq(
|
911
|
+
{ created_at: '2023-02-26 15:01:45 UTC',
|
912
|
+
title: 'Coffee',
|
913
|
+
price: 1000 }
|
914
|
+
)
|
915
|
+
```
|
916
|
+
|
917
|
+
And then you change something, and it breaks multiple tests because `'2023-02-26 15:01:45 UTC'` becomes `'2023-02-26 15:03:29 UTC'` and `1000` becomes `950`.
|
918
|
+
|
919
|
+
You already have other tests ensuring that the `created_at` and `price` are correct, and in this specific test, you just want to ensure that `to_h` generates the expected output _shape_.
|
920
|
+
|
921
|
+
So after patiently spending hours and hours fixing these little unimportant changes, you give up on creating new tests like these and eventually even deleting the old ones because it's too much work to maintain them, reducing your test coverage.
|
922
|
+
|
923
|
+
Well, this is happening because you are testing the **wrong** thing. You don't care about each minimal bit of the content in this test, only its _approximate shape_. This is similar to the idea of a [_Contract Test_](https://martinfowler.com/bliki/ContractTest.html).
|
924
|
+
|
925
|
+
To ensure that writing tests will be as painless as possible, we have a [helper for testing _contracts_](https://github.com/icebaker/lighstorm/blob/main/spec/helpers/contract_spec.rb):
|
926
|
+
|
927
|
+
```ruby
|
928
|
+
expect(Contract.for(something.to_h)).to eq(
|
929
|
+
{ created_at: 'String:21..30',
|
930
|
+
price: 'Integer:0..10',
|
931
|
+
title: 'String:0..10' }
|
932
|
+
)
|
933
|
+
```
|
934
|
+
|
935
|
+
That's it. No matter the date, your test will pass as long as `created_at` is a `String` between 21 and 30 characters.
|
936
|
+
|
937
|
+
Also, sometimes you end up with a test that contains a `Hash` with 100+ lines. While it's good to have the contract visible when it's short, if it's too long, we can just use a [hashed version](https://en.wikipedia.org/wiki/Hash_function) of the contract to ensure that it's not being broken:
|
938
|
+
|
939
|
+
```ruby
|
940
|
+
Contract.expect(
|
941
|
+
something.to_h, '77b0c3a51abe6'
|
942
|
+
) do |actual, expected|
|
943
|
+
expect(actual.hash).to eq(expected.hash)
|
944
|
+
expect(actual.contract).to eq(expected.contract)
|
945
|
+
end
|
946
|
+
```
|
947
|
+
|
948
|
+
It will generate a `.bin` file inside `spec/data/contracts/` containing the contract [_marshaled_](https://ruby-doc.org/core-3.0.0/Marshal.html), and if it changes, the `77b0c3a51abe6` hash will change, and your test will fail.
|
949
|
+
|
950
|
+
Inside the block, you can inspect the actual data with `actual.data`. For generating a contract for the first time, use `expect!` with a `nil` hash:
|
951
|
+
|
952
|
+
```ruby
|
953
|
+
Contract.expect!(
|
954
|
+
something.to_h, nil
|
955
|
+
) do |actual, expected|
|
956
|
+
expect(actual.hash).to eq(expected.hash)
|
957
|
+
expect(actual.contract).to eq(expected.contract)
|
958
|
+
end
|
959
|
+
```
|
960
|
+
|
961
|
+
Your contract will be generated, and you can get its hash from the `rspec` output:
|
962
|
+
|
963
|
+
```ruby
|
964
|
+
expected: nil
|
965
|
+
got: "77b0c3a51abe67133e981bc362430b2600d23200e9b3b335c890a975bda44575"
|
966
|
+
```
|
967
|
+
|
968
|
+
Remember to undo it afterward, replacing `expect!` with `expect`.
|
969
|
+
|
970
|
+
## Generating Documentation
|
971
|
+
|
972
|
+
```sh
|
973
|
+
npm i docsify-cli -g
|
974
|
+
|
975
|
+
docsify serve ./docs
|
976
|
+
```
|
977
|
+
|
978
|
+
## Publish to RubyGems
|
979
|
+
|
980
|
+
```sh
|
981
|
+
gem build lighstorm.gemspec
|
982
|
+
|
983
|
+
gem signin
|
984
|
+
|
985
|
+
gem push lighstorm-0.0.7.gem
|
986
|
+
```
|
987
|
+
|
723
988
|
_________________
|
724
989
|
|
725
990
|
<center>
|
726
|
-
lighstorm 0.0.
|
991
|
+
lighstorm 0.0.7
|
727
992
|
|
|
728
993
|
<a href="https://github.com/icebaker/lighstorm" rel="noopener noreferrer" target="_blank">GitHub</a>
|
729
994
|
|
|