lighstorm 0.0.6 → 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 +1 -1
- 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 +15 -0
- 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 -12
- data/controllers/invoice/actions/pay_through_route.rb +1 -1
- data/controllers/invoice.rb +3 -3
- data/docs/README.md +390 -145
- 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,17 +315,17 @@ 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
|
@@ -403,7 +337,7 @@ channel.myself.policy.fee.update(
|
|
403
337
|
)
|
404
338
|
|
405
339
|
channel.myself.policy.fee.update(
|
406
|
-
{ base: {
|
340
|
+
{ base: { millisatoshis: 1 } }
|
407
341
|
)
|
408
342
|
|
409
343
|
channel.myself.policy.fee.update(
|
@@ -411,7 +345,7 @@ channel.myself.policy.fee.update(
|
|
411
345
|
)
|
412
346
|
|
413
347
|
channel.myself.policy.fee.update(
|
414
|
-
{ base: {
|
348
|
+
{ base: { millisatoshis: 1 }, rate: { parts_per_million: 25 } }
|
415
349
|
)
|
416
350
|
```
|
417
351
|
|
@@ -444,7 +378,7 @@ invoice.state
|
|
444
378
|
# https://github.com/lightning/bolts/blob/master/11-payment-encoding.md
|
445
379
|
invoice.request.code # "lnbc20m1pv...qqdhhwkj"
|
446
380
|
|
447
|
-
invoice.request.amount.
|
381
|
+
invoice.request.amount.millisatoshis
|
448
382
|
|
449
383
|
invoice.request.description.memo
|
450
384
|
invoice.request.description.hash
|
@@ -456,20 +390,25 @@ invoice.request.secret.hash
|
|
456
390
|
invoice.request.address
|
457
391
|
```
|
458
392
|
|
459
|
-
###
|
393
|
+
### Create
|
460
394
|
|
461
395
|
[Understanding Lightning Invoices](https://docs.lightning.engineering/the-lightning-network/payment-lifecycle/understanding-lightning-invoices)
|
462
396
|
|
463
397
|
```ruby
|
464
398
|
# 'preview' let you check the expected operation
|
465
399
|
# before actually performing it for debug purposes
|
466
|
-
|
467
|
-
|
400
|
+
preview = Lighstorm::Invoice.create(
|
401
|
+
description: 'Coffee', millisatoshis: 1000, preview: true
|
468
402
|
)
|
469
403
|
|
470
|
-
|
471
|
-
|
404
|
+
action = Lighstorm::Invoice.create(
|
405
|
+
description: 'Piña Colada', millisatoshis: 1000
|
472
406
|
)
|
407
|
+
|
408
|
+
action.to_h
|
409
|
+
|
410
|
+
action.response
|
411
|
+
invoice = action.result
|
473
412
|
```
|
474
413
|
|
475
414
|
## Payment
|
@@ -504,15 +443,15 @@ payment.created_at
|
|
504
443
|
payment.settled_at
|
505
444
|
payment.purpose
|
506
445
|
|
507
|
-
payment.fee.
|
446
|
+
payment.fee.millisatoshis
|
508
447
|
payment.fee.parts_per_million(
|
509
|
-
payment.request.amount.
|
448
|
+
payment.request.amount.millisatoshis
|
510
449
|
)
|
511
450
|
|
512
451
|
# https://github.com/lightning/bolts/blob/master/11-payment-encoding.md
|
513
452
|
payment.request.code # "lnbc20m1pv...qqdhhwkj"
|
514
453
|
|
515
|
-
payment.request.amount.
|
454
|
+
payment.request.amount.millisatoshis
|
516
455
|
|
517
456
|
# https://docs.lightning.engineering/the-lightning-network/multihop-payments
|
518
457
|
payment.request.secret.preimage
|
@@ -524,9 +463,9 @@ payment.request.description.memo
|
|
524
463
|
payment.request.description.hash
|
525
464
|
|
526
465
|
payment.from.hop
|
527
|
-
payment.from.amount.
|
528
|
-
payment.from.fee.
|
529
|
-
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)
|
530
469
|
|
531
470
|
payment.from.channel.id
|
532
471
|
|
@@ -539,9 +478,9 @@ payment.from.channel.exit.alias
|
|
539
478
|
payment.from.channel.exit.color
|
540
479
|
|
541
480
|
payment.to.hop
|
542
|
-
payment.to.amount.
|
543
|
-
payment.to.fee.
|
544
|
-
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)
|
545
484
|
|
546
485
|
payment.to.channel.id
|
547
486
|
|
@@ -559,9 +498,9 @@ payment.hops[0].first?
|
|
559
498
|
payment.hops[0].last?
|
560
499
|
|
561
500
|
payment.hops[0].hop
|
562
|
-
payment.hops[0].amount.
|
563
|
-
payment.hops[0].fee.
|
564
|
-
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)
|
565
504
|
|
566
505
|
payment.hops[0].channel.id
|
567
506
|
|
@@ -611,19 +550,19 @@ forward._key
|
|
611
550
|
|
612
551
|
forward.at
|
613
552
|
|
614
|
-
forward.fee.
|
553
|
+
forward.fee.millisatoshis
|
615
554
|
forward.fee.parts_per_million(
|
616
|
-
forward.in.amount.
|
555
|
+
forward.in.amount.millisatoshis
|
617
556
|
)
|
618
557
|
|
619
|
-
forward.in.amount.
|
558
|
+
forward.in.amount.millisatoshis
|
620
559
|
|
621
560
|
forward.in.channel.id
|
622
561
|
forward.in.channel.partner.node.alias
|
623
562
|
forward.in.channel.partner.node.public_key
|
624
563
|
forward.in.channel.partner.node.color
|
625
564
|
|
626
|
-
forward.out.amount.
|
565
|
+
forward.out.amount.millisatoshis
|
627
566
|
|
628
567
|
forward.out.channel.id
|
629
568
|
forward.out.channel.partner.node.alias
|
@@ -646,12 +585,12 @@ group._key
|
|
646
585
|
|
647
586
|
group.last_at
|
648
587
|
group.analysis.count
|
649
|
-
group.analysis.sums.amount.
|
650
|
-
group.analysis.sums.fee.
|
651
|
-
group.analysis.averages.amount.
|
652
|
-
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
|
653
592
|
group.analysis.averages.fee.parts_per_million(
|
654
|
-
group.analysis.averages.amount.
|
593
|
+
group.analysis.averages.amount.millisatoshis
|
655
594
|
)
|
656
595
|
|
657
596
|
group.channel.id
|
@@ -725,11 +664,11 @@ Lighstorm::Channel.adapt(dump: channel.dump)
|
|
725
664
|
|
726
665
|
```ruby
|
727
666
|
Lighstorm::Satoshis
|
728
|
-
Lighstorm::Satoshis.new(
|
667
|
+
Lighstorm::Satoshis.new(millisatoshis: 75621650)
|
729
668
|
|
730
669
|
satoshis.to_h
|
731
670
|
|
732
|
-
satoshis.
|
671
|
+
satoshis.millisatoshis
|
733
672
|
satoshis.satoshis
|
734
673
|
satoshis.bitcoins
|
735
674
|
|
@@ -737,13 +676,319 @@ satoshis.msats
|
|
737
676
|
satoshis.sats
|
738
677
|
satoshis.btc
|
739
678
|
|
740
|
-
|
741
|
-
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
|
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
|
742
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
|
+
|
743
988
|
_________________
|
744
989
|
|
745
990
|
<center>
|
746
|
-
lighstorm 0.0.
|
991
|
+
lighstorm 0.0.7
|
747
992
|
|
|
748
993
|
<a href="https://github.com/icebaker/lighstorm" rel="noopener noreferrer" target="_blank">GitHub</a>
|
749
994
|
|
|