lighstorm 0.0.6 → 0.0.8

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +1 -1
  3. data/Gemfile.lock +5 -5
  4. data/README.md +17 -55
  5. data/adapters/connections/channel_node/fee.rb +1 -1
  6. data/adapters/connections/channel_node/policy.rb +6 -6
  7. data/adapters/connections/channel_node.rb +1 -1
  8. data/adapters/connections/payment_channel.rb +2 -2
  9. data/adapters/edges/channel.rb +7 -7
  10. data/adapters/edges/forward.rb +3 -3
  11. data/adapters/edges/payment.rb +1 -1
  12. data/adapters/invoice.rb +35 -0
  13. data/adapters/payment_request.rb +14 -3
  14. data/components/cache.rb +8 -5
  15. data/controllers/action.rb +24 -0
  16. data/controllers/channel/actions/apply_gossip.rb +18 -18
  17. data/controllers/channel/actions/update_fee.rb +33 -22
  18. data/controllers/forward/group_by_channel.rb +3 -3
  19. data/controllers/invoice/actions/create.rb +39 -12
  20. data/controllers/invoice/actions/pay_through_route.rb +1 -1
  21. data/controllers/invoice/decode.rb +44 -0
  22. data/controllers/invoice.rb +8 -3
  23. data/docs/README.md +392 -145
  24. data/docs/_coverpage.md +6 -1
  25. data/docs/index.html +1 -1
  26. data/models/connections/channel_node/accounting.rb +1 -1
  27. data/models/connections/channel_node/fee.rb +5 -4
  28. data/models/connections/channel_node/htlc.rb +4 -4
  29. data/models/connections/forward_channel.rb +1 -1
  30. data/models/connections/payment_channel.rb +4 -4
  31. data/models/edges/channel/accounting.rb +5 -5
  32. data/models/edges/forward.rb +3 -3
  33. data/models/edges/groups/channel_forwards/analysis.rb +8 -8
  34. data/models/edges/payment.rb +3 -3
  35. data/models/errors.rb +1 -1
  36. data/models/payment_request.rb +2 -2
  37. data/models/satoshis.rb +12 -12
  38. data/static/cache.rb +2 -0
  39. data/static/spec.rb +1 -1
  40. metadata +4 -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 [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.
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
- Although it tries to stay close to [Lightning's terminologies](https://docs.lightning.engineering/lightning-network-tools/lnd), it brings its own vocabulary and [data modeling](#data-modeling), optimizing for [programmer happiness](https://rubyonrails.org/doctrine).
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.6'
30
+ gem 'lighstorm', '~> 0.0.8'
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.6
63
+ puts Lighstorm.version # => 0.0.8
60
64
 
61
- Lighstorm::Satoshis.new(
62
- milisatoshis: 75_621_650
63
- ).satoshis # => 75_621
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.milisatoshis # => 75621650
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
- ## Channel
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.milisatoshis
123
+ channel.accounting.capacity.millisatoshis
116
124
 
117
- channel.partner.accounting.balance.milisatoshis
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.milisatoshis
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
- ## Forward
141
+ ### Forward
134
142
 
135
143
  ```ruby
136
144
  forward = Lighstorm::Forward.last
137
145
 
138
146
  forward.at
139
147
 
140
- forward.fee.milisatoshis
148
+ forward.fee.millisatoshis
141
149
  forward.fee.parts_per_million
142
150
 
143
- forward.in.amount.milisatoshis
144
- forward.out.amount.milisatoshis
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
- ## Payment
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.milisatoshis
179
+ payment.request.amount.millisatoshis
172
180
 
173
181
  payment.from.hop
174
- payment.from.amount.milisatoshis
175
- payment.from.fee.milisatoshis
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.milisatoshis
182
- payment.to.fee.milisatoshis
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.milisatoshis
188
- payment.hops[0].fee.milisatoshis
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.milisatoshis
342
- channel.accounting.sent.milisatoshis
343
- channel.accounting.received.milisatoshis
344
- channel.accounting.unsettled.milisatoshis
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.milisatoshis
301
+ channel.partner.accounting.balance.millisatoshis
368
302
 
369
- channel.partner.policy.fee.base.milisatoshis
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.milisatoshis
373
- channel.partner.policy.htlc.maximum.milisatoshis
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.milisatoshis
318
+ channel.myself.accounting.balance.millisatoshis
385
319
 
386
- channel.myself.policy.fee.base.milisatoshis
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.milisatoshis
390
- channel.myself.policy.htlc.maximum.milisatoshis
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
- ### Operations
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: { milisatoshis: 1 } }
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: { milisatoshis: 1 }, rate: { parts_per_million: 25 } }
348
+ { base: { millisatoshis: 1 }, rate: { parts_per_million: 25 } }
415
349
  )
416
350
  ```
417
351
 
@@ -426,6 +360,8 @@ Lighstorm::Invoice.all(limit: 10)
426
360
  Lighstorm::Invoice.first
427
361
  Lighstorm::Invoice.last
428
362
 
363
+ Lighstorm::Invoice.decode('lnbc20n1pj...0eqps7h0k9')
364
+
429
365
  Lighstorm::Invoice.find_by_secret_hash(
430
366
  '1d438b8100518c9fba0a607e3317d6b36f74ceef3a6591836eb2f679c6853501'
431
367
  )
@@ -444,7 +380,7 @@ invoice.state
444
380
  # https://github.com/lightning/bolts/blob/master/11-payment-encoding.md
445
381
  invoice.request.code # "lnbc20m1pv...qqdhhwkj"
446
382
 
447
- invoice.request.amount.milisatoshis
383
+ invoice.request.amount.millisatoshis
448
384
 
449
385
  invoice.request.description.memo
450
386
  invoice.request.description.hash
@@ -456,20 +392,25 @@ invoice.request.secret.hash
456
392
  invoice.request.address
457
393
  ```
458
394
 
459
- ### Operations
395
+ ### Create
460
396
 
461
397
  [Understanding Lightning Invoices](https://docs.lightning.engineering/the-lightning-network/payment-lifecycle/understanding-lightning-invoices)
462
398
 
463
399
  ```ruby
464
400
  # 'preview' let you check the expected operation
465
401
  # before actually performing it for debug purposes
466
- invoice = Lighstorm::Invoice.create(
467
- milisatoshis: 1000, description: 'Coffee', preview: true
402
+ preview = Lighstorm::Invoice.create(
403
+ description: 'Coffee', millisatoshis: 1000, preview: true
468
404
  )
469
405
 
470
- invoice = Lighstorm::Invoice.create(
471
- milisatoshis: 1000, description: 'Piña Colada'
406
+ action = Lighstorm::Invoice.create(
407
+ description: 'Piña Colada', millisatoshis: 1000
472
408
  )
409
+
410
+ action.to_h
411
+
412
+ action.response
413
+ invoice = action.result
473
414
  ```
474
415
 
475
416
  ## Payment
@@ -504,15 +445,15 @@ payment.created_at
504
445
  payment.settled_at
505
446
  payment.purpose
506
447
 
507
- payment.fee.milisatoshis
448
+ payment.fee.millisatoshis
508
449
  payment.fee.parts_per_million(
509
- payment.request.amount.milisatoshis
450
+ payment.request.amount.millisatoshis
510
451
  )
511
452
 
512
453
  # https://github.com/lightning/bolts/blob/master/11-payment-encoding.md
513
454
  payment.request.code # "lnbc20m1pv...qqdhhwkj"
514
455
 
515
- payment.request.amount.milisatoshis
456
+ payment.request.amount.millisatoshis
516
457
 
517
458
  # https://docs.lightning.engineering/the-lightning-network/multihop-payments
518
459
  payment.request.secret.preimage
@@ -524,9 +465,9 @@ payment.request.description.memo
524
465
  payment.request.description.hash
525
466
 
526
467
  payment.from.hop
527
- payment.from.amount.milisatoshis
528
- payment.from.fee.milisatoshis
529
- payment.from.fee.parts_per_million(payment.from.amount.milisatoshis)
468
+ payment.from.amount.millisatoshis
469
+ payment.from.fee.millisatoshis
470
+ payment.from.fee.parts_per_million(payment.from.amount.millisatoshis)
530
471
 
531
472
  payment.from.channel.id
532
473
 
@@ -539,9 +480,9 @@ payment.from.channel.exit.alias
539
480
  payment.from.channel.exit.color
540
481
 
541
482
  payment.to.hop
542
- payment.to.amount.milisatoshis
543
- payment.to.fee.milisatoshis
544
- payment.to.fee.parts_per_million(payment.to.amount.milisatoshis)
483
+ payment.to.amount.millisatoshis
484
+ payment.to.fee.millisatoshis
485
+ payment.to.fee.parts_per_million(payment.to.amount.millisatoshis)
545
486
 
546
487
  payment.to.channel.id
547
488
 
@@ -559,9 +500,9 @@ payment.hops[0].first?
559
500
  payment.hops[0].last?
560
501
 
561
502
  payment.hops[0].hop
562
- payment.hops[0].amount.milisatoshis
563
- payment.hops[0].fee.milisatoshis
564
- payment.hops[0].fee.parts_per_million(payment.hops[0].amount.milisatoshis)
503
+ payment.hops[0].amount.millisatoshis
504
+ payment.hops[0].fee.millisatoshis
505
+ payment.hops[0].fee.parts_per_million(payment.hops[0].amount.millisatoshis)
565
506
 
566
507
  payment.hops[0].channel.id
567
508
 
@@ -611,19 +552,19 @@ forward._key
611
552
 
612
553
  forward.at
613
554
 
614
- forward.fee.milisatoshis
555
+ forward.fee.millisatoshis
615
556
  forward.fee.parts_per_million(
616
- forward.in.amount.milisatoshis
557
+ forward.in.amount.millisatoshis
617
558
  )
618
559
 
619
- forward.in.amount.milisatoshis
560
+ forward.in.amount.millisatoshis
620
561
 
621
562
  forward.in.channel.id
622
563
  forward.in.channel.partner.node.alias
623
564
  forward.in.channel.partner.node.public_key
624
565
  forward.in.channel.partner.node.color
625
566
 
626
- forward.out.amount.milisatoshis
567
+ forward.out.amount.millisatoshis
627
568
 
628
569
  forward.out.channel.id
629
570
  forward.out.channel.partner.node.alias
@@ -646,12 +587,12 @@ group._key
646
587
 
647
588
  group.last_at
648
589
  group.analysis.count
649
- group.analysis.sums.amount.milisatoshis
650
- group.analysis.sums.fee.milisatoshis
651
- group.analysis.averages.amount.milisatoshis
652
- group.analysis.averages.fee.milisatoshis
590
+ group.analysis.sums.amount.millisatoshis
591
+ group.analysis.sums.fee.millisatoshis
592
+ group.analysis.averages.amount.millisatoshis
593
+ group.analysis.averages.fee.millisatoshis
653
594
  group.analysis.averages.fee.parts_per_million(
654
- group.analysis.averages.amount.milisatoshis
595
+ group.analysis.averages.amount.millisatoshis
655
596
  )
656
597
 
657
598
  group.channel.id
@@ -725,11 +666,11 @@ Lighstorm::Channel.adapt(dump: channel.dump)
725
666
 
726
667
  ```ruby
727
668
  Lighstorm::Satoshis
728
- Lighstorm::Satoshis.new(milisatoshis: 75_621_650)
669
+ Lighstorm::Satoshis.new(millisatoshis: 75621650)
729
670
 
730
671
  satoshis.to_h
731
672
 
732
- satoshis.milisatoshis
673
+ satoshis.millisatoshis
733
674
  satoshis.satoshis
734
675
  satoshis.bitcoins
735
676
 
@@ -737,13 +678,319 @@ satoshis.msats
737
678
  satoshis.sats
738
679
  satoshis.btc
739
680
 
740
- reference_in_milisatoshis = 75_621_650_000
741
- satoshis.parts_per_million(reference_in_milisatoshis)
681
+ reference_in_millisatoshis = 75621650000
682
+ satoshis.parts_per_million(reference_in_millisatoshis)
683
+ ```
684
+
685
+ # Error Handling
686
+
687
+ ## Rescuing
688
+ ```ruby
689
+ require 'lighstorm'
690
+
691
+ channel = Lighstorm::Channel.mine.first
692
+
693
+ begin
694
+ channel.myself.policy.fee.update(
695
+ { rate: { parts_per_million: -1 } }, preview: true
696
+ )
697
+ rescue Lighstorm::Errors::NegativeNotAllowedError => error
698
+ puts error.message # 'fee rate can't be negative: -1'
699
+ end
700
+
701
+ begin
702
+ channel.myself.policy.fee.update(
703
+ { rate: { parts_per_million: -1 } }, preview: true
704
+ )
705
+ rescue Lighstorm::Errors::LighstormError => error
706
+ puts error.message # 'fee rate can't be negative: -1'
707
+ end
708
+ ```
709
+
710
+ ### For Short
711
+
712
+ ```ruby
713
+ require 'lighstorm'
714
+ require 'lighstorm/errors'
715
+
716
+ channel = Lighstorm::Channel.mine.first
717
+
718
+ begin
719
+ channel.myself.policy.fee.update(
720
+ { rate: { parts_per_million: -1 } }, preview: true
721
+ )
722
+ rescue NegativeNotAllowedError => error
723
+ puts error.message # "fee rate can't be negative: -1"
724
+ end
725
+
726
+ begin
727
+ channel.myself.policy.fee.update(
728
+ { rate: { parts_per_million: -1 } }, preview: true
729
+ )
730
+ rescue LighstormError => error
731
+ puts error.message # "fee rate can't be negative: -1"
732
+ end
742
733
  ```
734
+
735
+ ## Errors
736
+ ```ruby
737
+ LighstormError
738
+
739
+ IncoherentGossipError
740
+
741
+ TooManyArgumentsError
742
+ MissingCredentialsError
743
+ MissingGossipHandlerError
744
+ MissingMillisatoshisError
745
+ MissingPartsPerMillionError
746
+ MissingTTLError
747
+
748
+ NegativeNotAllowedError
749
+
750
+ NotYourChannelError
751
+ NotYourNodeError
752
+ UnknownChannelError
753
+
754
+ OperationNotAllowedError
755
+ UnexpectedNumberOfHTLCsError
756
+ UpdateChannelPolicyError
757
+ ```
758
+
759
+ # Development
760
+
761
+ Copy the `.env.example` file to `.env` and provide the required data.
762
+
763
+ ```ruby
764
+ # Gemfile
765
+ gem 'lighstorm', path: '/home/user/lighstorm'
766
+
767
+ # demo.rb
768
+ require 'lighstorm'
769
+
770
+ puts Lighstorm.version # => 0.0.8
771
+ ```
772
+
773
+ ```sh
774
+ bundle
775
+ rubocop -A
776
+ ```
777
+
778
+ ## Testing
779
+
780
+ Copy the `.env.example` file to `.env` and provide the required data.
781
+
782
+ ```
783
+ bundle
784
+
785
+ bundle exec rspec
786
+ ```
787
+ ### Approach
788
+
789
+ 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).
790
+
791
+ 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.
792
+
793
+ 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.
794
+
795
+ Let's get practical.
796
+
797
+ #### Requesting
798
+
799
+ To _request_ all Nodes you:
800
+ ```ruby
801
+ Lighstorm::Node.all
802
+ ```
803
+
804
+ Internally, what's happening:
805
+ ```ruby
806
+ nodes = Lighstorm::Node.all
807
+
808
+ data = Controllers::Node::All.fetch # side effect
809
+ adapted = Controllers::Node::All.adapt(data) # pure
810
+ transformed = Controllers::Node::All.transform(adapted) # pure
811
+ models = Controllers::Node::All.model(transformed) # pure
812
+ nodes = models # pure
813
+
814
+ nodes.first.public_key
815
+ ```
816
+
817
+ So, `fetch` -> `adapt` -> `transform` -> `model`:
818
+
819
+ ![A diagram illustrating the Request Process described above.](https://raw.githubusercontent.com/icebaker/assets/main/lighstorm/request.png)
820
+
821
+ 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.
822
+
823
+ 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.
824
+
825
+ #### Performing Actions
826
+
827
+ To perform an _action_, like creating an Invoice, you:
828
+ ```ruby
829
+ Lighstorm::Invoice.create(
830
+ description: 'Coffee', millisatoshis: 1000
831
+ )
832
+ ```
833
+
834
+ Internally, what's happening:
835
+ ```ruby
836
+ action = Lighstorm::Invoice.create(description: 'Coffee', millisatoshis: 1000)
837
+
838
+ request = Controllers::Invoice::Create.prepare(params) # pure
839
+ response = Controllers::Invoice::Create.dispatch(request) # side effect
840
+ adapted = Controllers::Invoice::Create.adapt(response) # pure
841
+ data = Controllers::Invoice::Create.fetch(adapted) # side effect
842
+ model = Controllers::Invoice::Create.model(data) # pure
843
+ action = { response: response, result: model } # pure
844
+
845
+ invoice = action.result
846
+ ```
847
+
848
+ So, `prepare` -> `dispatch` -> `adapt` -> `fetch` -> `model`:
849
+
850
+ ![A diagram illustrating the Action Process described above.](https://raw.githubusercontent.com/icebaker/assets/main/lighstorm/action.png)
851
+
852
+ 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.
853
+
854
+ ### VCR
855
+
856
+ 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.
857
+
858
+ _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:
859
+
860
+ **Tape:**
861
+
862
+ 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).
863
+
864
+ **Reel:**
865
+
866
+ 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).
867
+
868
+ #### Replaying
869
+
870
+ To create a replayable _Tape_:
871
+
872
+ ```ruby
873
+ public_key = '02d3c80335a8ccb2ed364c06875f32240f36f7edb37d80f8dbe321b4c364b6e997'
874
+
875
+ data = VCR.tape.replay('lightning.get_node_info', pub_key: public_key) do
876
+ Lighstorm::Ports::GRPC.lightning.get_node_info(pub_key: public_key).to_h
877
+ end
878
+ ```
879
+
880
+ It will generate a `.bin` file inside `spec/data/tapes/` containing the data [_marshaled_](https://ruby-doc.org/core-3.0.0/Marshal.html).
881
+
882
+ If you want to force the overwrite of a Tape, replace `replay` with `replay!` Remember to undo it afterward, replacing `replay!` with `replay`.
883
+
884
+ To create a replayable _Reel_, just do all the same, but replace `VCR.tape` with `VCR.reel`:
885
+
886
+ ```ruby
887
+ response = VCR.reel.replay(
888
+ 'lightning.add_invoice',
889
+ memo: 'Coffee', value_msat: 1000
890
+ ) do
891
+ Lighstorm::Ports::GRPC.lightning.add_invoice(
892
+ memo: 'Coffee', value_msat: 1000
893
+ ).to_h
894
+ end
895
+ ```
896
+
897
+ 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.
898
+
899
+ #### Security
900
+
901
+ 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).
902
+
903
+ 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).
904
+
905
+ ### Contracts
906
+
907
+ 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.
908
+
909
+ It usually plays like this:
910
+
911
+ ```ruby
912
+ expect(something.to_h).to eq(
913
+ { created_at: '2023-02-26 15:01:45 UTC',
914
+ title: 'Coffee',
915
+ price: 1000 }
916
+ )
917
+ ```
918
+
919
+ 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`.
920
+
921
+ 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_.
922
+
923
+ 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.
924
+
925
+ 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).
926
+
927
+ 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):
928
+
929
+ ```ruby
930
+ expect(Contract.for(something.to_h)).to eq(
931
+ { created_at: 'String:21..30',
932
+ price: 'Integer:0..10',
933
+ title: 'String:0..10' }
934
+ )
935
+ ```
936
+
937
+ That's it. No matter the date, your test will pass as long as `created_at` is a `String` between 21 and 30 characters.
938
+
939
+ 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:
940
+
941
+ ```ruby
942
+ Contract.expect(
943
+ something.to_h, '77b0c3a51abe6'
944
+ ) do |actual, expected|
945
+ expect(actual.hash).to eq(expected.hash)
946
+ expect(actual.contract).to eq(expected.contract)
947
+ end
948
+ ```
949
+
950
+ 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.
951
+
952
+ 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:
953
+
954
+ ```ruby
955
+ Contract.expect!(
956
+ something.to_h, nil
957
+ ) do |actual, expected|
958
+ expect(actual.hash).to eq(expected.hash)
959
+ expect(actual.contract).to eq(expected.contract)
960
+ end
961
+ ```
962
+
963
+ Your contract will be generated, and you can get its hash from the `rspec` output:
964
+
965
+ ```ruby
966
+ expected: nil
967
+ got: "77b0c3a51abe67133e981bc362430b2600d23200e9b3b335c890a975bda44575"
968
+ ```
969
+
970
+ Remember to undo it afterward, replacing `expect!` with `expect`.
971
+
972
+ ## Generating Documentation
973
+
974
+ ```sh
975
+ npm i docsify-cli -g
976
+
977
+ docsify serve ./docs
978
+ ```
979
+
980
+ ## Publish to RubyGems
981
+
982
+ ```sh
983
+ gem build lighstorm.gemspec
984
+
985
+ gem signin
986
+
987
+ gem push lighstorm-0.0.8.gem
988
+ ```
989
+
743
990
  _________________
744
991
 
745
992
  <center>
746
- lighstorm 0.0.6
993
+ lighstorm 0.0.8
747
994
  |
748
995
  <a href="https://github.com/icebaker/lighstorm" rel="noopener noreferrer" target="_blank">GitHub</a>
749
996
  |