lighstorm 0.0.6 → 0.0.8
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +1 -1
- data/Gemfile.lock +5 -5
- 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 +35 -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/decode.rb +44 -0
- data/controllers/invoice.rb +8 -3
- data/docs/README.md +392 -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 +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 [
|
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.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.
|
63
|
+
puts Lighstorm.version # => 0.0.8
|
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
|
|
@@ -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.
|
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
|
-
###
|
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
|
-
|
467
|
-
|
402
|
+
preview = Lighstorm::Invoice.create(
|
403
|
+
description: 'Coffee', millisatoshis: 1000, preview: true
|
468
404
|
)
|
469
405
|
|
470
|
-
|
471
|
-
|
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.
|
448
|
+
payment.fee.millisatoshis
|
508
449
|
payment.fee.parts_per_million(
|
509
|
-
payment.request.amount.
|
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.
|
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.
|
528
|
-
payment.from.fee.
|
529
|
-
payment.from.fee.parts_per_million(payment.from.amount.
|
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.
|
543
|
-
payment.to.fee.
|
544
|
-
payment.to.fee.parts_per_million(payment.to.amount.
|
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.
|
563
|
-
payment.hops[0].fee.
|
564
|
-
payment.hops[0].fee.parts_per_million(payment.hops[0].amount.
|
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.
|
555
|
+
forward.fee.millisatoshis
|
615
556
|
forward.fee.parts_per_million(
|
616
|
-
forward.in.amount.
|
557
|
+
forward.in.amount.millisatoshis
|
617
558
|
)
|
618
559
|
|
619
|
-
forward.in.amount.
|
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.
|
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.
|
650
|
-
group.analysis.sums.fee.
|
651
|
-
group.analysis.averages.amount.
|
652
|
-
group.analysis.averages.fee.
|
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.
|
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(
|
669
|
+
Lighstorm::Satoshis.new(millisatoshis: 75621650)
|
729
670
|
|
730
671
|
satoshis.to_h
|
731
672
|
|
732
|
-
satoshis.
|
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
|
-
|
741
|
-
satoshis.parts_per_million(
|
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.
|
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
|
|
|