paid-services 3.2.0 → 3.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/.prettierignore +2 -0
  2. package/CHANGELOG.md +13 -0
  3. package/capacity/accept_capacity_change.js +284 -0
  4. package/capacity/ask_for_change_details.js +538 -0
  5. package/capacity/ask_for_decrease.js +185 -0
  6. package/capacity/assemble_replacement_tx.js +171 -0
  7. package/capacity/capacity_change_requests.js +85 -0
  8. package/capacity/change_channel_capacity.js +189 -0
  9. package/capacity/encode_change_request.js +62 -0
  10. package/capacity/encode_sign_capacity_response.js +32 -0
  11. package/capacity/fee_for_replacement.js +108 -0
  12. package/capacity/finalize_capacity_replacement.js +67 -0
  13. package/capacity/get_capacity_change_requests.js +99 -0
  14. package/capacity/get_capacity_replacement.js +372 -0
  15. package/capacity/get_replacement_signature.js +232 -0
  16. package/capacity/index.js +4 -0
  17. package/capacity/initiate_capacity_change.js +507 -0
  18. package/capacity/interim_replacement_psbt.js +57 -0
  19. package/capacity/is_open_witness_script.js +40 -0
  20. package/capacity/parse_accept_request.js +58 -0
  21. package/capacity/parse_accept_response.js +52 -0
  22. package/capacity/parse_capacity_change_request.js +108 -0
  23. package/capacity/parse_sign_capacity_request.js +59 -0
  24. package/capacity/propose_capacity_change.js +364 -0
  25. package/capacity/sign_capacity_replacement.js +119 -0
  26. package/capacity/witness_script_from_close_txs.js +50 -0
  27. package/index.js +2 -0
  28. package/p2p/encode_peer_response.js +0 -1
  29. package/p2p/make_peer_request.js +3 -3
  30. package/p2p/service_peer_requests.js +10 -2
  31. package/package.json +7 -4
  32. package/service_types.json +16 -0
  33. package/test/capacity/test_assemble_replacement_tx.js +46 -0
  34. package/test/capacity/test_capacity_change_requests.js +55 -0
  35. package/test/capacity/test_encode_change_request.js +54 -0
  36. package/test/capacity/test_encode_sign_capacity_response.js +47 -0
  37. package/test/capacity/test_fee_for_replacement.js +40 -0
  38. package/test/capacity/test_finalize_capacity_replacement.js +45 -0
  39. package/test/capacity/test_interim_replacement_psbt.js +44 -0
  40. package/test/capacity/test_is_open_witness_script.js +40 -0
  41. package/test/capacity/test_parse_accept_request.js +42 -0
  42. package/test/capacity/test_parse_capacity_change_request.js +52 -0
  43. package/test/capacity/test_parse_sign_capacity_request.js +49 -0
  44. package/test/capacity/test_witness_script_from_close_txs.js +53 -0
  45. package/test/integration/test_accept_capacity_change.js +189 -0
  46. package/test/integration/test_change_channel_capacity.js +203 -0
  47. package/test/integration/test_decrease_channel_capacity.js +216 -0
  48. package/test/integration/test_increase_channel_capacity.js +201 -0
  49. package/test/integration/test_sign_capacity_replacement.js +142 -0
  50. package/test/p2p/test_encode_peer_request.js +53 -0
  51. package/test/p2p/test_encode_peer_response.js +38 -0
  52. package/test/p2p/test_encode_response_code.js +48 -0
  53. package/test/trades/test_decode_basic_trade.js +40 -0
  54. package/test/trades/test_decode_open_trade.js +50 -0
  55. package/test/trades/test_encode_open_trade.js +35 -0
  56. package/test/trades/test_encode_trade.js +37 -0
  57. package/trades/accept_trade.js +52 -0
  58. package/trades/create_trade.js +124 -44
  59. package/trades/decode_basic_trade.js +45 -0
  60. package/trades/decode_open_trade.js +69 -0
  61. package/trades/decode_trade.js +48 -66
  62. package/trades/decode_trade_secret.js +89 -0
  63. package/trades/encode_open_trade.js +89 -0
  64. package/trades/encode_trade.js +48 -33
  65. package/trades/encode_trade_secret.js +50 -0
  66. package/trades/finalize_trade_secret.js +116 -0
  67. package/trades/find_trade.js +314 -0
  68. package/trades/manage_trade.js +46 -35
  69. package/trades/network_from_network_record.js +8 -2
  70. package/trades/network_record_from_network.js +36 -0
  71. package/trades/network_record_from_request.js +6 -0
  72. package/trades/node_as_record.js +98 -0
  73. package/trades/record_as_node.js +113 -0
  74. package/trades/request_trade_by_id.js +89 -0
  75. package/trades/service_trade_requests.js +184 -0
@@ -0,0 +1,538 @@
1
+ const asyncAuto = require('async/auto');
2
+ const asyncUntil = require('async/until');
3
+ const {findKey} = require('ln-sync');
4
+ const {getChainFeeRate} = require('ln-service');
5
+ const {getChainTransactions} = require('ln-service');
6
+ const {getChannel} = require('ln-service');
7
+ const {getChannels} = require('ln-service');
8
+ const {getPeers} = require('ln-service');
9
+ const {returnResult} = require('asyncjs-util');
10
+ const {sendMessageToPeer} = require('ln-service');
11
+
12
+ const askForDecrease = require('./ask_for_decrease');
13
+ const encodeChangeRequest = require('./encode_change_request');
14
+ const feeForReplacement = require('./fee_for_replacement');
15
+
16
+ const {ceil} = Math;
17
+ const dust = 550;
18
+ const dustBuffer = 750;
19
+ const isNumber = n => !isNaN(n);
20
+ const largeChannelsBit = 19;
21
+ const maxSats = 21e6 * 1e8;
22
+ const maxSize = 16777215;
23
+ const minNewLocalBalance = 0;
24
+ const nonNegative = n => Math.max(0, n);
25
+ const outputSize = 44;
26
+ const positive = n => Math.max(1, n);
27
+ const privateType = 1;
28
+ const publicType = 0;
29
+ const slowTarget = 1000;
30
+ const sumOf = arr => arr.reduce((sum, n) => sum + n, 0);
31
+ const sumOfTokens = arr => arr.reduce((sum, n) => sum + n.tokens, 0);
32
+ const testMessage = '00';
33
+ const tokensAsBigUnit = tokens => (tokens / 1e8).toFixed(8);
34
+ const weightAsVBytes = n => n / 4;
35
+ const weightBuffer = 150;
36
+
37
+ /** Ask for change details
38
+
39
+ {
40
+ ask: <Ask Function>
41
+ id: <Request Identifier Hex String>
42
+ lnd: <Authenticated LND API Object>
43
+ }
44
+
45
+ @returns via cbk or Promise
46
+ {
47
+ base_fee_mtokens: <Base Fee Millitokens String>
48
+ cltv_delta: <Locktime Delta Number>
49
+ coop_close_address: <Cooperative Close Address String>
50
+ decrease: [{
51
+ address: <Decrease Address String>
52
+ output: <Output Script Hex String>
53
+ tokens: <Decrease Tokens Number>
54
+ }]
55
+ estimated_capacity: <Estimated Tokens For New Channel Capacity Number>
56
+ estimated_local_delta: <Estimated Local Balance Delta Tokens Number>
57
+ fee_rate: <Fees Charged in Millitokens Per Million Number>
58
+ id: <Standard Format Channel Id String>
59
+ increase: <Channel Capacity Increase Tokens Number>
60
+ is_private: <Channel is Private Bool>
61
+ [open_transaction]: <Channel Open Transaction Hex String>
62
+ partner_csv_delay: <Peer CSV Delay Number>
63
+ partner_public_key: <Node to Ask For Capacity Change Public Key Hex String>
64
+ records: [{
65
+ type: <Change Record Type Number String>
66
+ value: <Change Record Value Hex Encoded String>
67
+ }]
68
+ remote_balance: <Peer Balance Tokens Number>
69
+ transaction_id: <Channel Funding Transaction Id Hex String>
70
+ }
71
+ */
72
+ module.exports = ({ask, id, lnd}, cbk) => {
73
+ return new Promise((resolve, reject) => {
74
+ return asyncAuto({
75
+ // Check arguments
76
+ validate: cbk => {
77
+ if (!ask) {
78
+ return cbk([400, 'ExpectedAskFunctionToAskForChangeDetails']);
79
+ }
80
+
81
+ if (!id) {
82
+ return cbk([400, 'ExpectedRequestIdentifierToAskForChangeDetails']);
83
+ }
84
+
85
+ if (!lnd) {
86
+ return cbk([400, 'ExpectedLndToAskForChangeDetails']);
87
+ }
88
+
89
+ return cbk();
90
+ },
91
+
92
+ // Select peer to adjust
93
+ askForPeer: ['validate', ({}, cbk) => {
94
+ return ask({
95
+ name: 'query',
96
+ message: 'Public key or alias of peer to change capacity with?',
97
+ type: 'input',
98
+ validate: input => !!input,
99
+ },
100
+ ({query}) => cbk(null, query));
101
+ }],
102
+
103
+ // Get channels to use to lookup a public key for a peer
104
+ getChannels: ['validate', ({}, cbk) => getChannels({lnd}, cbk)],
105
+
106
+ // Get feature info for peers to figure out maximum channel size
107
+ getFeatures: ['validate', ({}, cbk) => getPeers({lnd}, cbk)],
108
+
109
+ // Get the chain fee rate
110
+ getFeeRate: ['validate', ({}, cbk) => {
111
+ return getChainFeeRate({lnd, confirmation_target: slowTarget}, cbk);
112
+ }],
113
+
114
+ // Find the public key or alias entered
115
+ findKey: [
116
+ 'askForPeer',
117
+ 'getChannels',
118
+ ({askForPeer, getChannels}, cbk) =>
119
+ {
120
+ return findKey({
121
+ lnd,
122
+ channels: getChannels.channels,
123
+ query: askForPeer,
124
+ },
125
+ cbk);
126
+ }],
127
+
128
+ // Send a test message to the selected peer to confirm they are connected
129
+ testPeer: ['findKey', 'getChannels', ({findKey, getChannels}, cbk) => {
130
+ const hasPeer = !!getChannels.channels.find(channel => {
131
+ return channel.partner_public_key === findKey.public_key;
132
+ });
133
+
134
+ if (!hasPeer) {
135
+ return cbk([400, 'UnknownPeerToChangeChannelCapacityWith']);
136
+ }
137
+
138
+ return sendMessageToPeer({
139
+ lnd,
140
+ message: testMessage,
141
+ public_key: findKey.public_key,
142
+ },
143
+ err => {
144
+ if (!!err) {
145
+ return cbk([503, 'CannotCommunicateWithSelectedPeer', {err}]);
146
+ }
147
+
148
+ return cbk();
149
+ });
150
+ }],
151
+
152
+ // Select a channel to adjust when there are multiple choices
153
+ askForChannel: [
154
+ 'findKey',
155
+ 'getChannels',
156
+ 'testPeer',
157
+ ({findKey, getChannels}, cbk) =>
158
+ {
159
+ const channelsForPeer = getChannels.channels.filter(channel => {
160
+ return channel.partner_public_key === findKey.public_key;
161
+ });
162
+
163
+ const [channel, more] = channelsForPeer;
164
+
165
+ // Exit early when there is only one channel, no need to ask
166
+ if (!more) {
167
+ return cbk(null, channel.id);
168
+ }
169
+
170
+ const choices = channelsForPeer.map(channel => {
171
+ // Do not allow changes on inactive or coop close locked channels
172
+ const disabled = [
173
+ !!channel.cooperative_close_address,
174
+ !channel.is_active,
175
+ ];
176
+
177
+ return {
178
+ disabled: !!disabled.filter(n => !!n).length,
179
+ name: `${channel.id}: ${tokensAsBigUnit(channel.capacity)}`,
180
+ value: channel.id,
181
+ };
182
+ });
183
+
184
+ if (!choices.filter(n => !n.disabled).length) {
185
+ return cbk([400, 'NoSuitableChannelsToChangeCapacity']);
186
+ }
187
+
188
+ return ask({
189
+ choices,
190
+ loop: false,
191
+ message: 'Channel to change capacity?',
192
+ name: 'id',
193
+ type: 'list',
194
+ },
195
+ ({id}) => cbk(null, id));
196
+ }],
197
+
198
+ // Get the channel policy info
199
+ getPolicy: ['askForChannel', ({askForChannel}, cbk) => {
200
+ return getChannel({lnd, id: askForChannel}, cbk);
201
+ }],
202
+
203
+ // Get chain transactions
204
+ getTx: ['askForChannel', ({askForChannel}, cbk) => {
205
+ return getChainTransactions({lnd}, cbk);
206
+ }],
207
+
208
+ // Details of the channel to change capacity with
209
+ channel: [
210
+ 'askForChannel',
211
+ 'findKey',
212
+ 'getChannels',
213
+ 'getFeatures',
214
+ 'getFeeRate',
215
+ 'getPolicy',
216
+ 'getTx',
217
+ ({
218
+ askForChannel,
219
+ findKey,
220
+ getChannels,
221
+ getFeatures,
222
+ getFeeRate,
223
+ getPolicy,
224
+ getTx,
225
+ },
226
+ cbk) =>
227
+ {
228
+ const feeRate = getFeeRate.tokens_per_vbyte;
229
+ const id = askForChannel;
230
+ const peerKey = findKey.public_key;
231
+
232
+ const channel = getChannels.channels.find(n => n.id === id);
233
+
234
+ // Do not allow selecting channels that are in use
235
+ if (channel.pending_payments.length) {
236
+ return cbk([400, 'ChannelHasPendingPayments']);
237
+ }
238
+
239
+ const peer = getFeatures.peers.find(n => n.public_key === peerKey);
240
+
241
+ // The peer must be connected
242
+ if (!peer) {
243
+ return cbk([503, 'FailedToFindConnectedPeer']);
244
+ }
245
+
246
+ const policy = getPolicy.policies.find(n => n.public_key === peerKey);
247
+
248
+ // A routing policy must be known so that it can be reset later
249
+ if (!policy || !policy.cltv_delta) {
250
+ return cbk([503, 'CannotFindChannelRoutingPolicyDetails']);
251
+ }
252
+
253
+ const openTx = getTx.transactions.find(tx => {
254
+ return tx.id === channel.transaction_id;
255
+ });
256
+
257
+ // A peer that disallows large channels limits the increase allowable
258
+ const isLarge = peer.features.find(n => n.bit === largeChannelsBit);
259
+
260
+ // When decreasing, cannot decrease more than there are fund available
261
+ const maxDecreaseTokens = nonNegative(sumOf([
262
+ -channel.commit_transaction_fee,
263
+ -ceil(weightAsVBytes(weightBuffer) * feeRate),
264
+ -ceil(weightAsVBytes(channel.commit_transaction_weight) * feeRate),
265
+ channel.local_balance,
266
+ -dustBuffer,
267
+ ]));
268
+
269
+ // When increasing, cannot increase more than the peer supports
270
+ const maxIncrease = !!isLarge ? maxSats : maxSize - channel.capacity;
271
+
272
+ return cbk(null, {
273
+ base_fee_mtokens: policy.base_fee_mtokens,
274
+ capacity: channel.capacity,
275
+ cltv_delta: policy.cltv_delta,
276
+ commit_transaction_fee: channel.commit_transaction_fee,
277
+ commit_transaction_weight: channel.commit_transaction_weight,
278
+ coop_close_address: channel.cooperative_close_address,
279
+ fee_rate: policy.fee_rate,
280
+ id: channel.id,
281
+ is_private: channel.is_private,
282
+ local_balance: channel.local_balance,
283
+ open_transaction: !!openTx ? openTx.transaction : undefined,
284
+ max_decrease_tokens: maxDecreaseTokens,
285
+ max_increase_tokens: nonNegative(maxIncrease),
286
+ partner_public_key: channel.partner_public_key,
287
+ remote_balance: channel.remote_balance,
288
+ remote_csv: channel.remote_csv,
289
+ transaction_id: channel.transaction_id,
290
+ transaction_vout: channel.transaction_vout,
291
+ });
292
+ }],
293
+
294
+ // Choose whether to add or remove coins
295
+ askForDirection: ['channel', ({channel}, cbk) => {
296
+ return ask({
297
+ choices: [
298
+ {
299
+ disabled: !channel.max_increase_tokens,
300
+ name: `Increase capacity (currently ${channel.capacity})`,
301
+ value: 'increase',
302
+ },
303
+ {
304
+ disabled: !channel.max_decrease_tokens,
305
+ name: `Decrease capacity (limit ${channel.max_decrease_tokens})`,
306
+ value: 'decrease',
307
+ },
308
+ ],
309
+ message: 'How do you want to change the channel capacity?',
310
+ name: 'direction',
311
+ type: 'list',
312
+ },
313
+ ({direction}) => cbk(null, direction));
314
+ }],
315
+
316
+ // Ask for how much to decrease
317
+ askForDecrease: [
318
+ 'askForDirection',
319
+ 'channel',
320
+ ({askForDirection, channel}, cbk) =>
321
+ {
322
+ // Exit early when funds are being added
323
+ if (askForDirection !== 'decrease') {
324
+ return cbk();
325
+ }
326
+
327
+ const decreases = [];
328
+ const maximum = channel.max_decrease_tokens;
329
+
330
+ if (!maximum) {
331
+ return cbk([400, 'ChannelLocalBalanceTooLowToDecrease']);
332
+ }
333
+
334
+ return asyncUntil(
335
+ cbk => cbk(null, !!decreases.find(n => n.is_final)),
336
+ cbk => {
337
+ return askForDecrease({
338
+ ask,
339
+ lnd,
340
+ max: maximum - sumOf(decreases.map(n => n.tokens + outputSize)),
341
+ },
342
+ (err, res) => {
343
+ if (!!err) {
344
+ return cbk(err);
345
+ }
346
+
347
+ if (decreases.find(n => n.address === res.address)) {
348
+ return cbk([400, 'ExpectedUniqueAddressForSpend']);
349
+ }
350
+
351
+ return cbk(null, decreases.push(res));
352
+ });
353
+ },
354
+ err => {
355
+ if (!!err) {
356
+ return cbk(err);
357
+ }
358
+
359
+ const final = decreases
360
+ .filter(n => !!n.tokens)
361
+ .map(({address, output, tokens}) => ({address, output, tokens}));
362
+
363
+ return cbk(null, final);
364
+ }
365
+ );
366
+ }],
367
+
368
+ // Ask to see how much to add to the channel
369
+ askForIncrease: [
370
+ 'askForDirection',
371
+ 'channel',
372
+ ({askForDirection, channel}, cbk) =>
373
+ {
374
+ // Exit early when funds are being added
375
+ if (askForDirection !== 'increase') {
376
+ return cbk();
377
+ }
378
+
379
+ const balance = channel.local_balance;
380
+
381
+ return ask({
382
+ name: 'amount',
383
+ message: `Amount to add?`,
384
+ type: 'input',
385
+ validate: input => {
386
+ if (!isNumber(input) || !Number.isInteger(Number(input))) {
387
+ return false;
388
+ }
389
+
390
+ if (!!Number(input) && Number(input) < dust) {
391
+ return false;
392
+ }
393
+
394
+ return true;
395
+ },
396
+ },
397
+ ({amount}) => cbk(null, Number(amount)));
398
+ }],
399
+
400
+ // Confirm or change the announce status of the replacement channel
401
+ askForPublicPrivate: [
402
+ 'askForDecrease',
403
+ 'askForIncrease',
404
+ 'channel',
405
+ ({channel}, cbk) =>
406
+ {
407
+ return ask({
408
+ choices: ['Public', 'Private'].map(type => {
409
+ const isPrivate = type === 'Private';
410
+
411
+ // Exit early when the type would be different
412
+ if (isPrivate !== channel.is_private) {
413
+ return type;
414
+ }
415
+
416
+ return `${type} (Keep Current Status)`;
417
+ }),
418
+ default: channel.is_private ? 'Private' : 'Public',
419
+ message: 'Channel type?',
420
+ name: 'type',
421
+ type: 'list',
422
+ },
423
+ ({type}) => cbk(null, {is_private: type === 'Private'}));
424
+ }],
425
+
426
+ // Estimate the chain fee
427
+ estimateChainFee: [
428
+ 'askForDecrease',
429
+ 'askForIncrease',
430
+ 'askForPublicPrivate',
431
+ 'channel',
432
+ 'getFeeRate',
433
+ ({askForDecrease, askForIncrease, channel, getFeeRate}, cbk) =>
434
+ {
435
+ const {fee} = feeForReplacement({
436
+ capacity: channel.capacity,
437
+ commit_transaction_fee: channel.commit_transaction_fee,
438
+ commit_transaction_weight: channel.commit_transaction_weight,
439
+ decrease: askForDecrease || [],
440
+ increase: askForIncrease || undefined,
441
+ tokens_per_vbyte: getFeeRate.tokens_per_vbyte,
442
+ });
443
+
444
+ return cbk(null, {fee});
445
+ }],
446
+
447
+ // Confirm chain fee payment estimate
448
+ confirmFeeEstimate: [
449
+ 'askForPublicPrivate',
450
+ 'channel',
451
+ 'estimateChainFee',
452
+ ({channel, estimateChainFee}, cbk) =>
453
+ {
454
+ const {fee} = estimateChainFee;
455
+
456
+ const pay = `Pay estimated on-chain channel replacement fee: ${fee}`;
457
+
458
+ return ask({
459
+ type: 'confirm',
460
+ name: 'proceed',
461
+ message: `${pay} for capacity change?`,
462
+ },
463
+ ({proceed}) => cbk(null, proceed));
464
+ }],
465
+
466
+ // Final capacity change details
467
+ details: [
468
+ 'askForDecrease',
469
+ 'askForIncrease',
470
+ 'askForPublicPrivate',
471
+ 'channel',
472
+ 'confirmFeeEstimate',
473
+ 'estimateChainFee',
474
+ 'getTx',
475
+ ({
476
+ askForDecrease,
477
+ askForIncrease,
478
+ askForPublicPrivate,
479
+ channel,
480
+ confirmFeeEstimate,
481
+ estimateChainFee,
482
+ getTx,
483
+ },
484
+ cbk) =>
485
+ {
486
+ if (!confirmFeeEstimate) {
487
+ return cbk([400, 'RejectedCapacityChangeDueToFeeEstimate']);
488
+ }
489
+
490
+ // Estimate how much local balance will be adjusted down
491
+ const estimatedLocalDelta = sumOf([
492
+ -sumOfTokens(askForDecrease || []),
493
+ askForIncrease || Number(),
494
+ -estimateChainFee.fee,
495
+ ]);
496
+
497
+ // The new capacity is the old one with decreases/increases, minus fee
498
+ const estimatedNewCapacity = sumOf([
499
+ channel.capacity,
500
+ estimatedLocalDelta,
501
+ ]);
502
+
503
+ if (channel.local_balance + estimatedLocalDelta < minNewLocalBalance) {
504
+ return cbk([400, 'InsufficientFundsToChangeChannelCapacity']);
505
+ }
506
+
507
+ // The change will be communicated in change records
508
+ const {records} = encodeChangeRequest({
509
+ id,
510
+ channel: channel.id,
511
+ decrease: !!askForDecrease ? sumOfTokens(askForDecrease) : undefined,
512
+ increase: askForIncrease,
513
+ type: askForPublicPrivate.is_private ? privateType : publicType,
514
+ });
515
+
516
+ return cbk(null, {
517
+ records,
518
+ base_fee_mtokens: channel.base_fee_mtokens,
519
+ cltv_delta: channel.cltv_delta,
520
+ coop_close_address: channel.coop_close_address,
521
+ decrease: askForDecrease || [],
522
+ estimated_capacity: estimatedNewCapacity,
523
+ estimated_local_delta: estimatedLocalDelta,
524
+ fee_rate: channel.fee_rate,
525
+ id: channel.id,
526
+ increase: askForIncrease,
527
+ is_private: askForPublicPrivate.is_private,
528
+ open_transaction: channel.open_transaction,
529
+ partner_csv_delay: channel.partner_csv_delay,
530
+ partner_public_key: channel.partner_public_key,
531
+ remote_balance: channel.remote_balance,
532
+ transaction_id: channel.transaction_id,
533
+ });
534
+ }],
535
+ },
536
+ returnResult({reject, resolve, of: 'details'}, cbk));
537
+ });
538
+ };