paid-services 3.6.0 → 3.7.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.
@@ -1,15 +1,17 @@
1
1
  const asyncAuto = require('async/auto');
2
- const asyncDoUntil = require('async/doUntil');
2
+ const asyncEach = require('async/each');
3
3
  const asyncReflect = require('async/reflect');
4
4
  const {broadcastChainTransaction} = require('ln-service');
5
5
  const {cancelPendingChannel} = require('ln-service');
6
+ const {deletePendingChannel} = require('ln-service');
6
7
  const {fundPendingChannels} = require('ln-service');
7
- const {getChainTransactions} = require('ln-service');
8
8
  const {getChannels} = require('ln-service');
9
9
  const {getClosedChannels} = require('ln-service');
10
10
  const {getHeight} = require('ln-service');
11
+ const {getPendingChannels} = require('ln-service');
11
12
  const {returnResult} = require('asyncjs-util');
12
13
  const {signTransaction} = require('ln-service');
14
+ const {subscribeToBlocks} = require('ln-service');
13
15
  const {Transaction} = require('bitcoinjs-lib');
14
16
  const {transactionAsPsbt} = require('psbt');
15
17
 
@@ -21,6 +23,7 @@ const {serviceTypeSignCapacityChange} = require('./../service_types');
21
23
 
22
24
  const capacityChangeIdType = '0';
23
25
  const findSignatureRecord = records => records.find(n => n.type === '1');
26
+ const {fromHex} = Transaction;
24
27
  const fuzzHeight = 3;
25
28
  const maxSigHexLength = 146;
26
29
  const peerRequestTimeoutMs = 1000 * 60 * 10;
@@ -35,9 +38,10 @@ const unsignedTransactionType = '1';
35
38
  bitcoinjs_network: <BitcoinJS Network Name String>
36
39
  channel: <Channel to Change Standard Format Channel Id String>
37
40
  decrease: [{
38
- address: <Spend to Chain Address String>
39
- output: <Spend to Output Script Hex String>
40
- tokens: <Spend Value Number>
41
+ [address]: <Decrease Address String>
42
+ [node]: <Create Channel with Node With Public Key Hex String>
43
+ [output]: <Output Script Hex String>
44
+ tokens: <Decrease Tokens Number>
41
45
  }]
42
46
  id: <Capacity Change Request Id Hex String>
43
47
  is_private: <Channel is Private Bool>
@@ -109,10 +113,7 @@ module.exports = (args, cbk) => {
109
113
  return cbk();
110
114
  },
111
115
 
112
- // Get the starting height
113
- getHeight: ['validate', ({}, cbk) => getHeight({lnd: args.lnd}, cbk)],
114
-
115
- // Close the current channal and get the unsigned replacement
116
+ // Close the current channel and get the unsigned replacement
116
117
  getReplacement: ['validate', ({}, cbk) => {
117
118
  return getCapacityReplacement({
118
119
  add_funds_transaction_id: args.increase_transaction_id,
@@ -131,9 +132,9 @@ module.exports = (args, cbk) => {
131
132
  cbk);
132
133
  }],
133
134
 
134
- // Replacement channel outpoint
135
+ // The replacement channel outpoint will be returned as a result
135
136
  replacement: ['getReplacement', ({getReplacement}, cbk) => {
136
- args.logger.info({pending_chan_id: getReplacement.pending_channel_id});
137
+ args.logger.info({proposal_id: getReplacement.pending_channel_id});
137
138
 
138
139
  return cbk(null, {
139
140
  transaction_id: getReplacement.transaction_id,
@@ -164,8 +165,8 @@ module.exports = (args, cbk) => {
164
165
  cbk);
165
166
  }],
166
167
 
167
- // Use the unsigned replacement transaction to "fund" the pending channel
168
- fundWithReplacement: [
168
+ // Derive the funding PSBT to use for funding
169
+ funding: [
169
170
  'getReplacement',
170
171
  'signAddFunds',
171
172
  ({getReplacement, signAddFunds}, cbk) =>
@@ -183,10 +184,22 @@ module.exports = (args, cbk) => {
183
184
  witness_script: getReplacement.witness_script,
184
185
  });
185
186
 
186
- // Create the commitment transaction with the peer on the replacement
187
+ return cbk(null, psbt);
188
+ }],
189
+
190
+ // Use the unsigned replacement transaction to "fund" the pending channel
191
+ fundWithReplacement: [
192
+ 'funding',
193
+ 'getReplacement',
194
+ ({funding, getReplacement}, cbk) =>
195
+ {
196
+ const replacement = getReplacement.unsigned_transaction;
197
+
198
+ args.logger.info({funding_with_replacement_tx: replacement});
199
+
187
200
  return fundPendingChannels({
201
+ funding,
188
202
  channels: [getReplacement.pending_channel_id],
189
- funding: psbt,
190
203
  lnd: args.lnd,
191
204
  },
192
205
  cbk);
@@ -194,8 +207,8 @@ module.exports = (args, cbk) => {
194
207
 
195
208
  // Send the final go ahead signal now that a proposal is made
196
209
  getPeerSignature: [
197
- 'getReplacement',
198
210
  'fundWithReplacement',
211
+ 'getReplacement',
199
212
  asyncReflect(({getReplacement}, cbk) =>
200
213
  {
201
214
  return makePeerRequest({
@@ -229,49 +242,86 @@ module.exports = (args, cbk) => {
229
242
  return cbk([503, 'ExpectedSignatureRecordFromPeerForChange']);
230
243
  }
231
244
 
245
+ args.logger.info({peer_cosigned_capacity_change: true});
246
+
232
247
  return cbk(null, record.value);
233
248
  });
234
249
  })],
235
250
 
236
- // Cancel the funding proposal when there is an error
251
+ // Fund the additional other channel opens if present
252
+ fundAdditionalOpens: [
253
+ 'funding',
254
+ 'getPeerSignature',
255
+ 'getReplacement',
256
+ asyncReflect(({funding, getPeerSignature, getReplacement}, cbk) =>
257
+ {
258
+ // Exit early when there was an error getting the peer signature
259
+ if (!!getPeerSignature.error) {
260
+ return cbk();
261
+ }
262
+
263
+ // Exit early when there is nothing else to fund
264
+ if (!getReplacement.open_pending_ids.length) {
265
+ return cbk();
266
+ }
267
+
268
+ // Funding for the additional opens is separated for smoother canceling
269
+ return fundPendingChannels({
270
+ funding,
271
+ channels: getReplacement.open_pending_ids,
272
+ lnd: args.lnd,
273
+ },
274
+ cbk);
275
+ })],
276
+
277
+ // Cancel the unfunded channel opens when there is an error
237
278
  cancelFunding: [
238
279
  'getPeerSignature',
239
280
  'getReplacement',
240
281
  ({getPeerSignature, getReplacement}, cbk) =>
241
282
  {
242
- // Exit early when there is no need to cancel
283
+ // Exit early when there was an error getting the peer signature
243
284
  if (!getPeerSignature.error) {
244
285
  return cbk();
245
286
  }
246
287
 
247
- return cancelPendingChannel({
248
- id: getReplacement.pending_channel_id,
249
- lnd: args.lnd,
250
- },
251
- err => {
252
- if (!!err) {
253
- return cbk([503, 'FailedToCancelPendingChannel', {err}]);
254
- }
288
+ const ids = getReplacement.open_pending_ids;
255
289
 
256
- // Return the original error
257
- return cbk(getPeerSignature.error);
258
- });
290
+ return asyncEach(ids, asyncReflect((id, cbk) => {
291
+ return cancelPendingChannel({id, lnd: args.lnd}, cbk);
292
+ }),
293
+ cbk);
259
294
  }],
260
295
 
261
296
  // Broadcast the new funding transaction using the peer's signature
262
297
  confirmFunding: [
263
- 'getHeight',
298
+ 'fundAdditionalOpens',
264
299
  'getPeerSignature',
265
300
  'getReplacement',
266
301
  'signAddFunds',
267
- ({getHeight, getPeerSignature, getReplacement, signAddFunds}, cbk) =>
302
+ ({
303
+ fundAdditionalOpens,
304
+ getPeerSignature,
305
+ getReplacement,
306
+ signAddFunds,
307
+ },
308
+ cbk) =>
268
309
  {
269
- // Exit early when there was an error getting the peer signature
310
+ // When there is no peer signature, the force close must confirm
270
311
  if (!!getPeerSignature.error) {
271
- return cbk();
312
+ return cbk(null, {is_success: false});
313
+ }
314
+
315
+ // Do not broadcast a replacement when additional opens fail
316
+ if (!!fundAdditionalOpens.error) {
317
+ return cbk(null, {is_success: false});
272
318
  }
273
319
 
274
320
  const [addFundsSignature] = signAddFunds.signatures || [];
321
+ const openOutputs = fromHex(args.open_transaction).outs;
322
+ const txId = fromHex(getReplacement.unsigned_transaction).getId();
323
+
324
+ const outScript = openOutputs[args.transaction_vout].script;
275
325
 
276
326
  // Insert the peer signature to get the final signed funding tx
277
327
  const {transaction} = finalizeCapacityReplacement({
@@ -288,75 +338,140 @@ module.exports = (args, cbk) => {
288
338
 
289
339
  args.logger.info({publishing_new_channel_transaction: transaction});
290
340
 
291
- return asyncDoUntil(
292
- cbk => {
293
- // Try and override the force close tx using the replacement
294
- return broadcastChainTransaction({
295
- transaction,
296
- after: getHeight.current_block_height - fuzzHeight,
297
- lnd: args.lnd,
298
- },
299
- cbk);
300
- },
301
- (broadcast, cbk) => {
302
- args.logger.info({waiting_for_confirmation: broadcast.id});
341
+ // Listen to new blocks to wait for the channel change confirmation
342
+ const sub = subscribeToBlocks({lnd: args.lnd});
303
343
 
304
- // Rebroadcast until the transaction is confirmed
305
- return getChainTransactions({lnd: args.lnd}, (err, res) => {
306
- if (!!err) {
307
- return cbk(err);
308
- }
344
+ // Fail with error when the blocks subscription is lost
345
+ sub.on('error', err => {
346
+ sub.removeAllListeners();
347
+
348
+ return cbk([503, 'LostBlockchainSubscription', {err}]);
349
+ });
350
+
351
+ // Publish the new channel transaction when a block is received
352
+ sub.on('block', async () => {
353
+ // Broadcast the channel replacement tx
354
+ try {
355
+ await broadcastChainTransaction({transaction, lnd: args.lnd});
356
+
357
+ args.logger.info({published_new_channel_tx: txId});
358
+ } catch (err) {
359
+ args.logger.error({err});
360
+ }
361
+
362
+ // Look for the new channel to see if the change succeeded
363
+ try {
364
+ const {channels} = await getChannels({lnd: args.lnd});
309
365
 
310
- const confirmed = res.transactions.filter(n => !!n.is_confirmed);
366
+ const channel = channels.find(n => n.transaction_id === txId);
311
367
 
312
- // Find the confirmed tx
313
- const tx = confirmed.find(tx => tx.id === broadcast.id);
368
+ // Replacement is present but not yet active
369
+ if (!!channel && !channel.is_active) {
370
+ args.logger.info({waiting_for_channel_to_activate: true});
371
+ }
314
372
 
315
- if (!!tx) {
316
- return cbk(null, true);
373
+ // The new channel confirmed successfully and so the change worked
374
+ if (!!channel && !!channel.is_active) {
375
+ sub.removeAllListeners();
376
+
377
+ return cbk(null, {is_success: true});
378
+ }
379
+ } catch (err) {
380
+ args.logger.error({err});
381
+ }
382
+
383
+ // Also look for a force closed channel to see if the change failed
384
+ try {
385
+ const {channels} = await getClosedChannels({lnd: args.lnd});
386
+ const chainTip = await getHeight({lnd: args.lnd});
387
+
388
+ const currentHeight = chainTip.current_block_height;
389
+
390
+ const channel = channels.find(channel => {
391
+ // The closed channel must have a closed tx id
392
+ if (!channel.close_transaction_id) {
393
+ return false;
317
394
  }
318
395
 
319
- // Find the replacing tx
320
- const replacing = confirmed.find(tx => {
321
- return tx.id === getReplacement.force_close_tx_id;
322
- });
323
-
324
- // Cancel the pending channel if the force close confirms
325
- if (!!replacing) {
326
- return cancelPendingChannel({
327
- id: getReplacement.pending_channel_id,
328
- lnd: args.lnd,
329
- },
330
- () => {
331
- return cbk([503, 'FailedToReplaceForceCloseTransaction']);
332
- });
396
+ // Only consider channels that are conclusively closed
397
+ if (currentHeight - channel.close_confirm_height < fuzzHeight) {
398
+ return false;
333
399
  }
334
400
 
335
- return getChannels({
336
- lnd: args.lnd,
337
- partner_public_key: args.partner_public_key,
338
- },
339
- (err, res) => {
340
- if (!!err) {
341
- return cbk(err);
342
- }
343
-
344
- const channel = res.channels.find(channel => {
345
- return channel.transaction_id === broadcast.id
346
- });
347
-
348
- // Stop broadcasting when a channel shows up
349
- if (!!channel) {
350
- return cbk(null, true);
351
- }
352
-
353
- // Rebroadcast after a delay
354
- return setTimeout(() => cbk(null, false), rebroadcastDelayMs);
355
- });
401
+ // Find close channel matching the channel outpoint
402
+ if (channel.transaction_id !== args.transaction_id) {
403
+ return false;
404
+ }
405
+
406
+ return channel.transaction_vout === args.transaction_vout;
356
407
  });
408
+
409
+ // The old channel force closed and so the change fully failed
410
+ if (!!channel && channel.close_transaction_id !== txId) {
411
+ sub.removeAllListeners();
412
+
413
+ return cbk(null, {is_success: false});
414
+ }
415
+ } catch (err) {
416
+ args.logger.error({err});
417
+ }
418
+
419
+ return;
420
+ });
421
+
422
+ return;
423
+ }],
424
+
425
+ // Get pending channels to clean up if there was a failure
426
+ getPending: ['confirmFunding', ({confirmFunding}, cbk) => {
427
+ // Exit early when there was no failure
428
+ if (!!confirmFunding.is_success) {
429
+ return cbk();
430
+ }
431
+
432
+ return getPendingChannels({lnd: args.lnd}, cbk);
433
+ }],
434
+
435
+ // Clean up pending channels that will never confirm
436
+ cleanPendingChannels: [
437
+ 'confirmFunding',
438
+ 'getPending',
439
+ 'getReplacement',
440
+ ({confirmFunding, getPending, getReplacement}, cbk) =>
441
+ {
442
+ // Exit early when the channel activated
443
+ if (!!confirmFunding.is_success) {
444
+ args.logger.info({channel_capacity_changed: true});
445
+
446
+ return cbk();
447
+ }
448
+
449
+ const replaceId = fromHex(getReplacement.unsigned_transaction).getId();
450
+
451
+ // Collect all the pending channels that depend on the replacement
452
+ const pending = getPending.pending_channels
453
+ .filter(n => !!n.is_opening)
454
+ .filter(n => n.transaction_id === replaceId);
455
+
456
+ // Delete the pending channels that can never confirm
457
+ return asyncEach(pending, (channel, cbk) => {
458
+ return deletePendingChannel({
459
+ confirmed_transaction: getReplacement.force_close_tx,
460
+ lnd: args.lnd,
461
+ pending_transaction: getReplacement.unsigned_transaction,
462
+ pending_transaction_vout: channel.transaction_vout,
357
463
  },
358
- cbk
359
- );
464
+ err => {
465
+ if (!!err) {
466
+ args.logger.error({err});
467
+ }
468
+
469
+ return cbk();
470
+ });
471
+ },
472
+ () => {
473
+ return cbk([503, 'FailedToChangeChannelCapacity']);
474
+ });
360
475
  }],
361
476
  },
362
477
  returnResult({reject, resolve, of: 'replacement'}, cbk));
@@ -0,0 +1,54 @@
1
+ const {acceptsChannelOpen} = require('ln-sync');
2
+ const asyncAuto = require('async/auto');
3
+ const {connectPeer} = require('ln-sync');
4
+ const {returnResult} = require('asyncjs-util');
5
+
6
+ /** Check that a channel open would be accepted by a node
7
+
8
+ {
9
+ capacity: <Capacity Tokens Number>
10
+ id: <Node Identity Public Key Hex String>
11
+ lnd: <Authenticated LND API Object>
12
+ }
13
+
14
+ @returns via cbk or Promise
15
+ {
16
+ is_accepted: <Channel Proposal Is Accepted Bool>
17
+ }
18
+ */
19
+ module.exports = ({capacity, id, lnd}, cbk) => {
20
+ return new Promise((resolve, reject) => {
21
+ return asyncAuto({
22
+ // Check arguments
23
+ validate: cbk => {
24
+ if (!capacity) {
25
+ return cbk([400, 'ExpectedCapacityTokensToProposeChannelOpen']);
26
+ }
27
+
28
+ if (!id) {
29
+ return cbk([400, 'ExpectedNodePublicKeyToProposeChannelOpen']);
30
+ }
31
+
32
+ if (!lnd) {
33
+ return cbk([400, 'ExpectedAuthenticatedLndToProposeChannelOpen']);
34
+ }
35
+
36
+ return cbk();
37
+ },
38
+
39
+ // Connect to the peer
40
+ connect: ['validate', ({}, cbk) => connectPeer({id, lnd}, cbk)],
41
+
42
+ // Propose a channel open to check acceptance
43
+ checkAccept: ['connect', ({}, cbk) => {
44
+ return acceptsChannelOpen({
45
+ capacity,
46
+ lnd,
47
+ partner_public_key: id,
48
+ },
49
+ cbk);
50
+ }],
51
+ },
52
+ returnResult({reject, resolve, of: 'checkAccept'}, cbk));
53
+ });
54
+ };
package/package.json CHANGED
@@ -37,8 +37,8 @@
37
37
  "url": "https://github.com/alexbosworth/paid-services.git"
38
38
  },
39
39
  "scripts": {
40
- "integration-tests": "tap --branches=1 --functions=1 --lines=1 --statements=1 -t 120 test/integration/*.js",
40
+ "integration-tests": "tap --branches=1 --functions=1 --lines=1 --statements=1 -t 180 test/integration/*.js",
41
41
  "test": "tap --branches=1 --functions=1 --lines=1 --statements=1 -t 60 test/actions/*.js test/capacity/*.js test/client/*.js test/config/*.js test/p2p/*.js test/records/*.js test/respond/*.js test/server/*.js test/server/*.js test/services/*.js test/trades/*.js"
42
42
  },
43
- "version": "3.6.0"
43
+ "version": "3.7.0"
44
44
  }
@@ -60,15 +60,15 @@ test(`Accept capacity replacement`, async ({end, equal, strictSame}) => {
60
60
 
61
61
  // Wait for the channel to be active
62
62
  const channel = await asyncRetry({interval, times}, async () => {
63
- const [channel] = (await getChannels({lnd, is_active: true})).channels;
63
+ const [channel] = (await getChannels({lnd})).channels;
64
64
 
65
- if (!!channel) {
65
+ if (!!channel && !!channel.is_active) {
66
66
  return channel;
67
67
  }
68
68
 
69
69
  await generate({});
70
70
 
71
- throw new Error('ExpectedChannelActivation');
71
+ throw new Error('ExpectedInitialChannelActivation');
72
72
  });
73
73
 
74
74
  // Find the open tx
@@ -87,6 +87,10 @@ test(`Accept capacity replacement`, async ({end, equal, strictSame}) => {
87
87
  return cbk({amount: '0'});
88
88
  }
89
89
 
90
+ if (args.name === 'decrease') {
91
+ return cbk({decrease: 'internal_spend_funds'});
92
+ }
93
+
90
94
  if (args.name === 'direction') {
91
95
  return cbk({direction: 'decrease'});
92
96
  }
@@ -89,6 +89,10 @@ test(`Decrease capacity replacement`, async ({end, equal, strictSame}) => {
89
89
  return cbk({amount: '200000'});
90
90
  }
91
91
 
92
+ if (args.name === 'decrease') {
93
+ return cbk({decrease: 'internal_spend_funds'});
94
+ }
95
+
92
96
  if (args.name === 'direction') {
93
97
  return cbk({direction: 'decrease'});
94
98
  }
@@ -129,7 +133,9 @@ test(`Decrease capacity replacement`, async ({end, equal, strictSame}) => {
129
133
  // Wait to see the inbound proposal
130
134
  waitForProposal: async () => {
131
135
  await asyncRetry({interval, times}, async () => {
132
- const {requests} = await getCapacityChangeRequests({lnd: target.lnd});
136
+ const {requests} = await getCapacityChangeRequests({
137
+ lnd: target.lnd,
138
+ });
133
139
 
134
140
  if (!requests.length) {
135
141
  throw new Error('FailedToFindChangeRequest');