paid-services 3.2.0 → 3.3.0

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