lighstorm 0.0.6 → 0.0.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +1 -1
  3. data/README.md +17 -55
  4. data/adapters/connections/channel_node/fee.rb +1 -1
  5. data/adapters/connections/channel_node/policy.rb +6 -6
  6. data/adapters/connections/channel_node.rb +1 -1
  7. data/adapters/connections/payment_channel.rb +2 -2
  8. data/adapters/edges/channel.rb +7 -7
  9. data/adapters/edges/forward.rb +3 -3
  10. data/adapters/edges/payment.rb +1 -1
  11. data/adapters/invoice.rb +15 -0
  12. data/adapters/payment_request.rb +14 -3
  13. data/components/cache.rb +8 -5
  14. data/controllers/action.rb +24 -0
  15. data/controllers/channel/actions/apply_gossip.rb +18 -18
  16. data/controllers/channel/actions/update_fee.rb +33 -22
  17. data/controllers/forward/group_by_channel.rb +3 -3
  18. data/controllers/invoice/actions/create.rb +39 -12
  19. data/controllers/invoice/actions/pay_through_route.rb +1 -1
  20. data/controllers/invoice.rb +3 -3
  21. data/docs/README.md +390 -145
  22. data/docs/_coverpage.md +6 -1
  23. data/docs/index.html +1 -1
  24. data/models/connections/channel_node/accounting.rb +1 -1
  25. data/models/connections/channel_node/fee.rb +5 -4
  26. data/models/connections/channel_node/htlc.rb +4 -4
  27. data/models/connections/forward_channel.rb +1 -1
  28. data/models/connections/payment_channel.rb +4 -4
  29. data/models/edges/channel/accounting.rb +5 -5
  30. data/models/edges/forward.rb +3 -3
  31. data/models/edges/groups/channel_forwards/analysis.rb +8 -8
  32. data/models/edges/payment.rb +3 -3
  33. data/models/errors.rb +1 -1
  34. data/models/payment_request.rb +2 -2
  35. data/models/satoshis.rb +12 -12
  36. data/static/cache.rb +2 -0
  37. data/static/spec.rb +1 -1
  38. 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 [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.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.6
63
+ puts Lighstorm.version # => 0.0.7
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
 
@@ -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.milisatoshis
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
- ### Operations
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
- invoice = Lighstorm::Invoice.create(
467
- milisatoshis: 1000, description: 'Coffee', preview: true
400
+ preview = Lighstorm::Invoice.create(
401
+ description: 'Coffee', millisatoshis: 1000, preview: true
468
402
  )
469
403
 
470
- invoice = Lighstorm::Invoice.create(
471
- milisatoshis: 1000, description: 'Piña Colada'
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.milisatoshis
446
+ payment.fee.millisatoshis
508
447
  payment.fee.parts_per_million(
509
- payment.request.amount.milisatoshis
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.milisatoshis
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.milisatoshis
528
- payment.from.fee.milisatoshis
529
- payment.from.fee.parts_per_million(payment.from.amount.milisatoshis)
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.milisatoshis
543
- payment.to.fee.milisatoshis
544
- payment.to.fee.parts_per_million(payment.to.amount.milisatoshis)
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.milisatoshis
563
- payment.hops[0].fee.milisatoshis
564
- payment.hops[0].fee.parts_per_million(payment.hops[0].amount.milisatoshis)
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.milisatoshis
553
+ forward.fee.millisatoshis
615
554
  forward.fee.parts_per_million(
616
- forward.in.amount.milisatoshis
555
+ forward.in.amount.millisatoshis
617
556
  )
618
557
 
619
- forward.in.amount.milisatoshis
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.milisatoshis
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.milisatoshis
650
- group.analysis.sums.fee.milisatoshis
651
- group.analysis.averages.amount.milisatoshis
652
- group.analysis.averages.fee.milisatoshis
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.milisatoshis
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(milisatoshis: 75_621_650)
667
+ Lighstorm::Satoshis.new(millisatoshis: 75621650)
729
668
 
730
669
  satoshis.to_h
731
670
 
732
- satoshis.milisatoshis
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
- reference_in_milisatoshis = 75_621_650_000
741
- satoshis.parts_per_million(reference_in_milisatoshis)
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.6
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
  |