balanceofsatoshis 11.49.2 → 11.52.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Versions
2
2
 
3
+ ## 11.52.0
4
+
5
+ - `call`: Add command `getMasterPublicKeys` to list extended public keys
6
+
7
+ ## 11.51.0
8
+
9
+ - `telegram`: Support notifications when a channel is pending closing
10
+
11
+ ## 11.50.1
12
+
13
+ - `telegram`: Support forwards and payments with `--use-proxy`
14
+
15
+ ## 11.50.0
16
+
17
+ - `open`: Add `--opening-node` to batch open channels with multiple saved nodes
18
+
3
19
  ## 11.49.2
4
20
 
5
21
  - `open`: Fix crash when using `--set-fee-rate` but policy details are missing
package/bos CHANGED
@@ -44,6 +44,7 @@ const wallets = importLazy('./wallets');
44
44
  const {version} = importLazy('./package');
45
45
 
46
46
  const {BOOL} = prog;
47
+ const collect = arr => [].concat(...[arr]).filter(n => !!n);
47
48
  const {exit} = process;
48
49
  const flatten = arr => [].concat(...arr);
49
50
  const {FLOAT} = prog;
@@ -1050,27 +1051,27 @@ prog
1050
1051
  .option('--coop-close-address <addr>', 'Coop-close address', REPEATABLE)
1051
1052
  .option('--external-funding', 'Use external funds for the channel open')
1052
1053
  .option('--give <give_amount>', 'Amount to gift to peer', REPEATABLE)
1053
- .option('--node <node_name>', 'Node to open channels')
1054
+ .option('--node <node_name>', 'Saved node to open channels')
1055
+ .option('--opening-node <node_name>', 'Open with saved node', REPEATABLE)
1054
1056
  .option('--set-fee-rate <ppm>', 'Set forward fee rate to peer', REPEATABLE)
1055
1057
  .option('--type <type>', 'Type of channel (private/public)', REPEATABLE)
1056
1058
  .action((args, options, logger) => {
1057
1059
  return new Promise(async (resolve, reject) => {
1058
- const collect = n => flatten([n]).filter(n => !!n);
1059
-
1060
1060
  try {
1061
1061
  return peers.openChannels({
1062
1062
  logger,
1063
1063
  ask: (n, cbk) => inquirer.prompt([n]).then(res => cbk(res)),
1064
- capacities: flatten([options.amount].filter(n => !!n)),
1064
+ capacities: collect(options.amount),
1065
1065
  cooperative_close_addresses: collect(options.coopCloseAddress),
1066
1066
  fs: {getFile: readFile},
1067
- gives: flatten([options.give].filter(n => !!n)),
1067
+ gives: collect(options.give),
1068
1068
  is_external: options.externalFunding,
1069
1069
  lnd: (await lnd.authenticatedLnd({logger, node: options.node})).lnd,
1070
+ opening_nodes: collect(options.openingNode),
1070
1071
  public_keys: args.peerPublicKeys,
1071
1072
  request: commands.simpleRequest,
1072
- set_fee_rates: flatten([options.setFeeRate]).filter(n => !!n),
1073
- types: flatten([options.type].filter(n => !!n)),
1073
+ set_fee_rates: collect(options.setFeeRate),
1074
+ types: collect(options.type),
1074
1075
  },
1075
1076
  responses.returnObject({logger, reject, resolve}));
1076
1077
  } catch (err) {
package/commands/api.json CHANGED
@@ -427,6 +427,9 @@
427
427
  {
428
428
  "method": "getLockedUtxos"
429
429
  },
430
+ {
431
+ "method": "getMasterPublicKeys"
432
+ },
430
433
  {
431
434
  "method": "getMethods"
432
435
  },
package/package.json CHANGED
@@ -37,7 +37,7 @@
37
37
  "ln-accounting": "5.0.5",
38
38
  "ln-service": "53.8.0",
39
39
  "ln-sync": "3.10.0",
40
- "ln-telegram": "3.15.1",
40
+ "ln-telegram": "3.16.0",
41
41
  "moment": "2.29.1",
42
42
  "paid-services": "3.11.0",
43
43
  "probing": "2.0.3",
@@ -81,5 +81,5 @@
81
81
  "postpublish": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t alexbosworth/balanceofsatoshis --push .",
82
82
  "test": "tap --branches=1 --functions=1 --lines=1 --statements=1 -t 60 test/arrays/*.js test/balances/*.js test/chain/*.js test/display/*.js test/encryption/*.js test/lnd/*.js test/network/*.js test/nodes/*.js test/peers/*.js test/responses/*.js test/routing/*.js test/services/*.js test/swaps/*.js test/tags/*.js test/wallets/*.js"
83
83
  },
84
- "version": "11.49.2"
84
+ "version": "11.52.0"
85
85
  }
@@ -1,4 +1,5 @@
1
1
  const defaultChannelCapacity = 5e6;
2
+ const uniq = arr => Array.from(new Set(arr));
2
3
 
3
4
  /** Derive channel to open details from channel argument list
4
5
 
@@ -6,32 +7,48 @@ const defaultChannelCapacity = 5e6;
6
7
  addresses: [<Address String>]
7
8
  capacities: [<Channel Capacity Tokens Number>]
8
9
  gives: [<Give Tokens String>]
9
- nodes: [<Node Identity Public Key Hex String>]
10
+ nodes: [<Channel Partner Node Identity Public Key Hex String>]
11
+ rates: [<Set Fee Rate String>]
12
+ saved: [<Open on Saved Node Name String>]
10
13
  types: [<Channel Type String>]
11
14
  }
12
15
 
13
16
  @returns
14
17
  {
15
- channels: [{
16
- capacity: <Channel Capacity Tokens Number>
17
- [give_tokens]: <Give Tokens Number>
18
- is_private: <Channel Is Private Bool>
19
- partner_public_key: <Channel Partner Identity Public Key Hex String>
18
+ opens: [{
19
+ channels: [{
20
+ capacity: <Channel Capacity Tokens Number>
21
+ [cooperative_close_address]: <Restrict Coop Close to Address String>
22
+ [give_tokens]: <Give Tokens Number>
23
+ is_private: <Channel Is Private Bool>
24
+ partner_public_key: <Channel Partner Identity Public Key Hex String>
25
+ [rate]: <Set Fee Rate String>
26
+ }]
27
+ [node]: <Saved Node Name String>
20
28
  }]
21
29
  }
22
30
  */
23
- module.exports = ({addresses, capacities, gives, nodes, types}) => {
24
- const channels = nodes.map((key, i) => {
25
- const type = types[i] || undefined;
26
-
31
+ module.exports = args => {
32
+ const channels = args.nodes.map((key, i) => {
27
33
  return {
28
- capacity: capacities[i] || defaultChannelCapacity,
29
- cooperative_close_address: !!addresses[i] ? addresses[i] : undefined,
30
- give_tokens: !!gives[i] ? Number(gives[i]) : undefined,
31
- is_private: !!type && type === 'private',
34
+ capacity: args.capacities[i] || defaultChannelCapacity,
35
+ cooperative_close_address: args.addresses[i] || undefined,
36
+ give_tokens: !!args.gives[i] ? Number(args.gives[i]) : undefined,
37
+ is_private: !!args.types[i] && args.types[i] === 'private',
38
+ node: args.saved[i] || undefined,
32
39
  partner_public_key: key,
40
+ rate: args.rates[i] || undefined,
33
41
  };
34
42
  });
35
43
 
36
- return {channels};
44
+ // Exit early when there are no saved nodes to use
45
+ if (!args.saved.length) {
46
+ return {opens: [{channels}]};
47
+ }
48
+
49
+ const opens = uniq(args.saved).map(node => {
50
+ return {node, channels: channels.filter(n => n.node === node)};
51
+ });
52
+
53
+ return {opens};
37
54
  };
@@ -1,5 +1,6 @@
1
1
  const {randomBytes} = require('crypto');
2
2
 
3
+ const {acceptsChannelOpen} = require('ln-sync');
3
4
  const {addPeer} = require('ln-service');
4
5
  const {address} = require('bitcoinjs-lib');
5
6
  const {askForFeeRate} = require('ln-sync');
@@ -8,8 +9,10 @@ const asyncEach = require('async/each');
8
9
  const asyncEachSeries = require('async/eachSeries');
9
10
  const asyncDetectSeries = require('async/detectSeries');
10
11
  const asyncMap = require('async/map');
12
+ const asyncMapSeries = require('async/mapSeries');
11
13
  const asyncReflect = require('async/reflect');
12
14
  const asyncRetry = require('async/retry');
15
+ const {broadcastChainTransaction} = require('ln-service');
13
16
  const {cancelPendingChannel} = require('ln-service');
14
17
  const {fundPendingChannels} = require('ln-service');
15
18
  const {getFundedTransaction} = require('ln-sync');
@@ -26,16 +29,19 @@ const {Transaction} = require('bitcoinjs-lib');
26
29
  const {unlockUtxo} = require('ln-service');
27
30
 
28
31
  const adjustFees = require('./../routing/adjust_fees');
32
+ const {authenticatedLnd} = require('./../lnd');
29
33
  const channelsFromArguments = require('./channels_from_arguments');
30
34
  const {getAddressUtxo} = require('./../chain');
31
35
  const {parseAmount} = require('./../display');
32
36
 
33
37
  const bech32AsData = bech32 => address.fromBech32(bech32).data;
38
+ const detectNetworks = ['btc', 'btctestnet'];
39
+ const flatten = arr => [].concat(...arr);
34
40
  const format = 'p2wpkh';
35
41
  const {isArray} = Array;
36
42
  const isPublicKey = n => !!n && /^0[2-3][0-9A-F]{64}$/i.test(n);
37
- const lineBreak = '\n';
38
43
  const knownTypes = ['private', 'public'];
44
+ const lineBreak = '\n';
39
45
  const noInternalFundingVersions = ['0.11.0-beta', '0.11.1-beta'];
40
46
  const notFound = -1;
41
47
  const peerAddedDelayMs = 1000 * 5;
@@ -60,6 +66,7 @@ const utxoPollingTimes = 20;
60
66
  [is_external]: <Use External Funds to Open Channels Bool>
61
67
  lnd: <Authenticated LND API Object>
62
68
  logger: <Winston Logger Object>
69
+ opening_nodes: [<Open New Channel With Saved Node Name String>]
63
70
  public_keys: [<Public Key Hex String>]
64
71
  request: <Request Function>
65
72
  set_fee_rates: [<Fee Rate Number>]
@@ -100,6 +107,10 @@ module.exports = (args, cbk) => {
100
107
  return cbk([400, 'ExpectedLoggerToInitiateOpenChannelRequests']);
101
108
  }
102
109
 
110
+ if (!isArray(args.opening_nodes)) {
111
+ return cbk([400, 'ExpectedOpeningNodesArrayToInitiateOpenChannels']);
112
+ }
113
+
103
114
  if (!isArray(args.public_keys)) {
104
115
  return cbk([400, 'ExpectedPublicKeysToOpenChannels']);
105
116
  }
@@ -112,6 +123,7 @@ module.exports = (args, cbk) => {
112
123
  const hasCapacities = !!args.capacities.length;
113
124
  const hasGives = !!args.gives.length;
114
125
  const hasFeeRates = !!args.set_fee_rates.length;
126
+ const hasNodes = !!args.opening_nodes.length;
115
127
  const publicKeysLength = args.public_keys.length;
116
128
 
117
129
  if (!!hasCapacities && publicKeysLength !== args.capacities.length) {
@@ -130,6 +142,10 @@ module.exports = (args, cbk) => {
130
142
  return cbk([400, 'MustSetFeeRateForEveryPublicKey']);
131
143
  }
132
144
 
145
+ if (!!hasNodes && publicKeysLength !== args.opening_nodes.length) {
146
+ return cbk([400, 'MustSetOpeningNodeForEveryPublicKey']);
147
+ }
148
+
133
149
  if (!args.request) {
134
150
  return cbk([400, 'ExpectedRequestFunctionToOpenChannels']);
135
151
  }
@@ -162,13 +178,33 @@ module.exports = (args, cbk) => {
162
178
  return cbk(null, capacities);
163
179
  }],
164
180
 
165
- // Get network name
181
+ // Get LNDs associated with nodes specified for opening
182
+ getLnds: ['validate', ({}, cbk) => {
183
+ // Exit early when there are no opening nodes specified
184
+ if (!args.opening_nodes.length) {
185
+ return cbk(null, [{lnd: args.lnd}]);
186
+ }
187
+
188
+ return asyncMapSeries(uniq(args.opening_nodes), (node, cbk) => {
189
+ return authenticatedLnd({node, logger: args.logger}, (err, res) => {
190
+ if (!!err) {
191
+ return cbk(err);
192
+ }
193
+
194
+ return cbk(null, {node, lnd: res.lnd});
195
+ });
196
+ },
197
+ cbk);
198
+ }],
199
+
200
+ // Get the default network name
166
201
  getNetwork: ['validate', ({}, cbk) => getNetwork({lnd: args.lnd}, cbk)],
167
202
 
168
203
  // Get sockets in case we need to connect
169
204
  getNodes: ['validate', ({}, cbk) => {
170
205
  return asyncMap(uniq(args.public_keys), (key, cbk) => {
171
206
  return getNode({lnd: args.lnd, public_key: key}, (err, res) => {
207
+ // Ignore errors when a node is unknown in the graph
172
208
  if (!!err) {
173
209
  return cbk(null, {public_key: key, sockets: []});
174
210
  }
@@ -192,35 +228,89 @@ module.exports = (args, cbk) => {
192
228
  cbk);
193
229
  }],
194
230
 
195
- // Get connected peers to see if we are already connected
196
- getPeers: ['validate', ({}, cbk) => getPeers({lnd: args.lnd}, cbk)],
197
-
198
- // Get the wallet version and check if it is compatible
231
+ // Get the wallet version to make sure the node supports internal funding
199
232
  getWalletVersion: ['validate', ({}, cbk) => {
200
- return getWalletVersion({lnd: args.lnd}, (err, res) => {
201
- if (!!err) {
202
- return cbk([400, 'BackingLndCannotBeUsedToOpenChannels', {err}]);
203
- }
233
+ return getWalletVersion({lnd: args.lnd}, cbk);
234
+ }],
204
235
 
205
- return cbk(null, {version: res.version});
206
- });
236
+ // Get the networks of the opening nodes
237
+ getOpeningNetworks: ['getLnds', ({getLnds}, cbk) => {
238
+ if (!getLnds) {
239
+ return cbk();
240
+ }
241
+
242
+ return asyncMap(getLnds, ({lnd}, cbk) => getNetwork({lnd}, cbk), cbk);
207
243
  }],
208
244
 
209
- // Connect up to the peers
210
- connect: [
211
- 'capacities',
212
- 'getNodes',
213
- 'getPeers',
214
- ({capacities, getNodes, getPeers}, cbk) =>
215
- {
216
- const {channels} = channelsFromArguments({
245
+ // Get the opening parameters to use to open the new channels
246
+ opens: ['capacities', ({capacities}, cbk) => {
247
+ const {opens} = channelsFromArguments({
217
248
  capacities,
218
249
  addresses: args.cooperative_close_addresses,
219
250
  gives: args.gives,
220
251
  nodes: args.public_keys,
252
+ rates: args.set_fee_rates,
253
+ saved: args.opening_nodes,
221
254
  types: args.types,
222
255
  });
223
256
 
257
+ return cbk(null, opens);
258
+ }],
259
+
260
+ // Check if all networks are the same
261
+ checkNetworks: [
262
+ 'getNetwork',
263
+ 'getOpeningNetworks',
264
+ ({getNetwork, getOpeningNetworks}, cbk) =>
265
+ {
266
+ // Exit early when there are no networks to check
267
+ if (!getOpeningNetworks) {
268
+ return cbk();
269
+ }
270
+
271
+ if (!!getOpeningNetworks.find(n => n.network !== getNetwork.network)) {
272
+ return cbk([400, 'AllOpeningNodesMustBeOnSameChain']);
273
+ }
274
+
275
+ return cbk();
276
+ }],
277
+
278
+ // Get connected peers to see if we are already connected
279
+ getPeers: ['getLnds', ({getLnds}, cbk) => {
280
+ // Exit early when there are no opening nodes
281
+ if (!args.opening_nodes.length) {
282
+ return getPeers({lnd: args.lnd}, (err, res) => {
283
+ if (!!err) {
284
+ return cbk(err);
285
+ }
286
+
287
+ return cbk(null, [{peers: res.peers}]);
288
+ });
289
+ }
290
+
291
+ return asyncMap(args.opening_nodes, (node, cbk) => {
292
+ const {lnd} = getLnds.find(n => n.node === node);
293
+
294
+ return getPeers({lnd}, (err, res) => {
295
+ if (!!err) {
296
+ return cbk(err);
297
+ }
298
+
299
+ return cbk(null, {node, peers: res.peers});
300
+ });
301
+ },
302
+ cbk);
303
+ }],
304
+
305
+ // Connect up to the peers
306
+ connect: [
307
+ 'getLnds',
308
+ 'getNodes',
309
+ 'getPeers',
310
+ 'opens',
311
+ ({getLnds, getNodes, getPeers, opens}, cbk) =>
312
+ {
313
+ // Collect some details about nodes being connected to
224
314
  const nodes = getNodes.filter(n => !!n.channels_count).map(node => {
225
315
  return {
226
316
  node: `${node.alias || node.public_key}`,
@@ -231,49 +321,67 @@ module.exports = (args, cbk) => {
231
321
 
232
322
  args.logger.info(nodes);
233
323
 
234
- const openingTo = getNodes.map(node => {
235
- const {capacity} = channels.find(channel => {
236
- return channel.partner_public_key === node.public_key;
237
- });
324
+ // Connect up as peers
325
+ return asyncEach(opens, ({node, channels}, cbk) => {
326
+ // Summarize who is being opened to
327
+ const openingTo = getNodes
328
+ .filter(remote => {
329
+ return !!channels.find(channel => {
330
+ return channel.partner_public_key === remote.public_key;
331
+ });
332
+ })
333
+ .map(remote => {
334
+ const {capacity} = channels.find(channel => {
335
+ return channel.partner_public_key === remote.public_key;
336
+ });
238
337
 
239
- return `${node.alias || node.public_key}: ${tokAsBigUnit(capacity)}`;
240
- });
338
+ const remoteNamed = remote.alias || remote.public_key;
241
339
 
242
- args.logger.info({opening_to: openingTo});
340
+ return `${remoteNamed}: ${tokAsBigUnit(capacity)}`;
341
+ });
243
342
 
244
- return asyncEach(args.public_keys, (key, cbk) => {
245
- // Exit early when the peer is already connected
246
- if (getPeers.peers.map(n => n.public_key).includes(key)) {
247
- return cbk();
248
- }
343
+ args.logger.info({node, opening_to: openingTo});
249
344
 
250
- const node = getNodes.find(n => n.public_key === key);
345
+ const connectToKeys = channels.map(n => n.partner_public_key);
346
+ const {lnd} = getLnds.find(n => n.node === node);
347
+ const {peers} = getPeers.find(n => n.node === node);
251
348
 
252
- if (!node.sockets.length) {
253
- return cbk([503, 'NoAddressFoundToConnectToNode', {node}]);
254
- }
255
-
256
- args.logger.info({
257
- connecting_to: {alias: node.alias, public_key: node.public_key},
258
- });
349
+ return asyncEach(connectToKeys, (key, cbk) => {
350
+ // Exit early when the peer is already connected
351
+ if (peers.map(n => n.public_key).includes(key)) {
352
+ return cbk();
353
+ }
259
354
 
260
- return asyncRetry({times}, cbk => {
261
- return asyncDetectSeries(node.sockets, ({socket}, cbk) => {
262
- return addPeer({socket, lnd: args.lnd, public_key: key}, err => {
263
- return cbk(null, !err);
264
- });
265
- },
266
- (err, res) => {
267
- if (!!err) {
268
- return cbk(err);
269
- }
355
+ const to = getNodes.find(n => n.public_key === key);
270
356
 
271
- if (!res) {
272
- return cbk([503, 'FailedToConnectToPeer', ({peer: key})]);
273
- }
357
+ if (!to.sockets.length) {
358
+ return cbk([503, 'NoAddressFoundToConnectToNode', {to}]);
359
+ }
274
360
 
275
- return setTimeout(() => cbk(null, true), peerAddedDelayMs);
361
+ args.logger.info({
362
+ connecting_to: {alias: to.alias, public_key: to.public_key},
363
+ from: node,
276
364
  });
365
+
366
+ return asyncRetry({times}, cbk => {
367
+ return asyncDetectSeries(to.sockets, ({socket}, cbk) => {
368
+ return addPeer({lnd, socket, public_key: key}, err => {
369
+ return cbk(null, !err);
370
+ });
371
+ },
372
+ (err, res) => {
373
+ if (!!err) {
374
+ return cbk(err);
375
+ }
376
+
377
+ if (!res) {
378
+ return cbk([503, 'FailedToConnectToPeer', ({peer: key})]);
379
+ }
380
+
381
+ return setTimeout(() => cbk(null, true), peerAddedDelayMs);
382
+ });
383
+ },
384
+ cbk);
277
385
  },
278
386
  cbk);
279
387
  },
@@ -282,40 +390,33 @@ module.exports = (args, cbk) => {
282
390
 
283
391
  // Check all nodes that they will allow an inbound channel
284
392
  checkAcceptance: [
285
- 'capacities',
286
393
  'connect',
287
- ({capacities, connect}, cbk) =>
394
+ 'getLnds',
395
+ 'opens',
396
+ ({connect, getLnds, opens}, cbk) =>
288
397
  {
289
- const {channels} = channelsFromArguments({
290
- capacities,
291
- addresses: args.cooperative_close_addresses,
292
- gives: args.gives,
293
- nodes: args.public_keys,
294
- types: args.types,
398
+ // Flatten out the opens so that they can be tried serially
399
+ const tests = opens.map(({channels, node}) => {
400
+ return channels.map(channel => ({
401
+ capacity: channel.capacity,
402
+ cooperative_close_address: channel.cooperative_close_address,
403
+ give_tokens: channel.give_tokens,
404
+ is_private: channel.is_private,
405
+ lnd: getLnds.find(n => n.node === node).lnd,
406
+ partner_public_key: channel.partner_public_key,
407
+ }));
295
408
  });
296
409
 
297
- return asyncEachSeries(channels, (channel, cbk) => {
298
- const to = channel.partner_public_key;
299
-
300
- return openChannels({
301
- channels: [channel],
302
- lnd: args.lnd,
410
+ return asyncEachSeries(flatten(tests), (test, cbk) => {
411
+ return acceptsChannelOpen({
412
+ capacity: test.capacity,
413
+ cooperative_close_address: test.cooperative_close_address,
414
+ give_tokens: test.give_tokens,
415
+ is_private: test.is_private,
416
+ lnd: test.lnd,
417
+ partner_public_key: test.partner_public_key,
303
418
  },
304
- (err, res) => {
305
- if (!!err) {
306
- return cbk([503, 'UnexpectedErrorProposingChannel', {to, err}]);
307
- }
308
-
309
- const [{id}] = res.pending;
310
-
311
- return cancelPendingChannel({id, lnd: args.lnd}, (err, res) => {
312
- if (!!err) {
313
- return cbk([503, 'UnexpectedErrorCancelingChannel', {err}]);
314
- }
315
-
316
- return cbk(null, false);
317
- });
318
- });
419
+ cbk);
319
420
  },
320
421
  cbk);
321
422
  }],
@@ -341,7 +442,7 @@ module.exports = (args, cbk) => {
341
442
  // Peers are connected - what type of funding will be used?
342
443
  args.logger.info(lineBreak);
343
444
 
344
- // Prompt to make sure that internal funding should really be used
445
+ // Prompt to make sure that internal funding should be used
345
446
  return args.ask({
346
447
  default: true,
347
448
  message: 'Use internal wallet funds?',
@@ -366,39 +467,77 @@ module.exports = (args, cbk) => {
366
467
  'askForFeeRate',
367
468
  'capacities',
368
469
  'connect',
369
- 'getWalletVersion',
470
+ 'getLnds',
471
+ 'getNodes',
370
472
  'isExternal',
371
- ({capacities}, cbk) =>
473
+ 'opens',
474
+ ({getLnds, getNodes, opens}, cbk) =>
372
475
  {
373
- const {channels} = channelsFromArguments({
374
- capacities,
375
- addresses: args.cooperative_close_addresses,
376
- gives: args.gives,
377
- nodes: args.public_keys,
378
- types: args.types,
379
- });
476
+ // When there are multiple batches, broadcasting must be stopped
477
+ const [, hasMultipleBatches] = opens;
380
478
 
381
- return openChannels({channels, lnd: args.lnd}, (err, res) => {
382
- if (!!err) {
383
- return cbk(err);
479
+ // Go through each batch and open channels
480
+ return asyncMapSeries(opens, asyncReflect(({channels, node}, cbk) => {
481
+ const {lnd} = getLnds.find(n => n.node === node);
482
+
483
+ return openChannels({
484
+ channels,
485
+ lnd,
486
+ is_avoiding_broadcast: !!hasMultipleBatches,
487
+ },
488
+ (err, res) => {
489
+ if (!!err) {
490
+ return cbk(err);
491
+ }
492
+
493
+ return cbk(null, {lnd, node, pending: res.pending});
494
+ });
495
+ }),
496
+ (err, res) => {
497
+ const openError = res.find(n => !!n.error);
498
+ const opening = res.map(n => n.value).filter(n => !!n);
499
+
500
+ if (!!openError) {
501
+ // Cancel past successful batch channel open proposals
502
+ return asyncEach(opening, ({lnd, pending}, cbk) => {
503
+ return asyncEach(pending, ({id}, cbk) => {
504
+ return cancelPendingChannel({id, lnd}, err => {
505
+ // Suppress errors
506
+ return cbk();
507
+ });
508
+ },
509
+ cbk);
510
+ },
511
+ () => {
512
+ // Return the original error
513
+ return cbk(openError.error);
514
+ });
384
515
  }
385
516
 
386
- const pending = res.pending.slice();
517
+ return cbk(null, res.map(n => n.value));
518
+ });
519
+ }],
520
+
521
+ // Pending channel outputs
522
+ outputs: ['openChannels', ({openChannels}, cbk) => {
523
+ // All batches will be paid out together in a single tx
524
+ const pending = flatten(openChannels.map(({pending}) => {
525
+ return pending.map(n => ({address: n.address, tokens: n.tokens}));
526
+ }));
387
527
 
388
- // Sort outputs using BIP 69
389
- try {
528
+ // Sort all the outputs using BIP 69
529
+ try {
390
530
  pending.sort((a, b) => {
391
- // Sort by tokens ascending when no tie breaker needed
392
- if (a.tokens !== b.tokens) {
393
- return a.tokens - b.tokens;
394
- }
531
+ // Sort by tokens ascending when no tie breaker needed
532
+ if (a.tokens !== b.tokens) {
533
+ return a.tokens - b.tokens;
534
+ }
395
535
 
396
- return bech32AsData(a.address).compare(bech32AsData(b.address));
397
- });
398
- } catch (err) {}
536
+ return bech32AsData(a.address).compare(bech32AsData(b.address));
537
+ });
538
+ } catch (err) {}
399
539
 
400
- return cbk(null, {pending});
401
- });
540
+ return cbk(null, pending);
402
541
  }],
403
542
 
404
543
  // Detect funding transaction
@@ -407,13 +546,19 @@ module.exports = (args, cbk) => {
407
546
  'openChannels',
408
547
  ({getNetwork, openChannels}, cbk) =>
409
548
  {
549
+ if (!detectNetworks.includes(getNetwork.network)) {
550
+ return cbk();
551
+ }
552
+
553
+ const [{pending}] = openChannels;
554
+
555
+ const [{address, tokens}] = pending;
556
+
410
557
  return asyncRetry({
411
558
  interval: utxoPollingIntervalMs,
412
559
  times: utxoPollingTimes,
413
560
  },
414
561
  cbk => {
415
- const [{address, tokens}] = openChannels.pending;
416
-
417
562
  return getAddressUtxo({
418
563
  address,
419
564
  tokens,
@@ -445,12 +590,15 @@ module.exports = (args, cbk) => {
445
590
  funding_detected: Transaction.fromHex(foundTx).getId(),
446
591
  });
447
592
 
448
- return fundPendingChannels({
449
- channels: openChannels.pending.map(n => n.id),
450
- funding: res.psbt,
451
- lnd: args.lnd,
593
+ return asyncEach(openChannels, ({lnd, node, pending}, cbk) => {
594
+ return fundPendingChannels({
595
+ lnd,
596
+ channels: pending.map(n => n.id),
597
+ funding: res.psbt,
598
+ },
599
+ () => cbk());
452
600
  },
453
- () => cbk());
601
+ cbk);
454
602
  });
455
603
  });
456
604
  },
@@ -464,8 +612,8 @@ module.exports = (args, cbk) => {
464
612
  getFunding: [
465
613
  'askForFeeRate',
466
614
  'isExternal',
467
- 'openChannels',
468
- asyncReflect(({askForFeeRate, isExternal, openChannels}, cbk) =>
615
+ 'outputs',
616
+ asyncReflect(({askForFeeRate, isExternal, outputs}, cbk) =>
469
617
  {
470
618
  // Warn external funding that funds are expected within 10 minutes
471
619
  if (!!isExternal) {
@@ -475,15 +623,12 @@ module.exports = (args, cbk) => {
475
623
  }
476
624
 
477
625
  return getFundedTransaction({
626
+ outputs,
478
627
  ask: args.ask,
479
628
  chain_fee_tokens_per_vbyte: askForFeeRate.tokens_per_vbyte,
480
629
  is_external: isExternal,
481
630
  lnd: args.lnd,
482
631
  logger: args.logger,
483
- outputs: openChannels.pending.map(({address, tokens}) => ({
484
- address,
485
- tokens,
486
- })),
487
632
  },
488
633
  cbk);
489
634
  })],
@@ -528,25 +673,65 @@ module.exports = (args, cbk) => {
528
673
  fundChannels: [
529
674
  'fundingPsbt',
530
675
  'openChannels',
531
- asyncReflect(({fundingPsbt, openChannels}, cbk) =>
676
+ 'outputs',
677
+ asyncReflect(({fundingPsbt, openChannels, outputs}, cbk) =>
532
678
  {
533
679
  // Exit early when there is no funding PSBT
534
680
  if (!fundingPsbt.value || !fundingPsbt.value.psbt) {
535
681
  return cbk(null, {});
536
682
  }
537
683
 
538
- args.logger.info({
539
- funding: openChannels.pending.map(n => tokAsBigUnit(n.tokens)),
540
- });
684
+ args.logger.info({funding: outputs.map(n => tokAsBigUnit(n.tokens))});
541
685
 
542
- return fundPendingChannels({
543
- channels: openChannels.pending.map(n => n.id),
544
- funding: fundingPsbt.value.psbt,
545
- lnd: args.lnd,
686
+ return asyncMap(openChannels, ({lnd, node, pending}, cbk) => {
687
+ return fundPendingChannels({
688
+ lnd,
689
+ channels: pending.map(n => n.id),
690
+ funding: fundingPsbt.value.psbt,
691
+ },
692
+ cbk);
546
693
  },
547
694
  cbk);
548
695
  })],
549
696
 
697
+ // Broadcast the funding transaction when opening on multiple nodes
698
+ broadcastChainTransaction: [
699
+ 'fundChannels',
700
+ 'fundingPsbt',
701
+ 'getFunding',
702
+ 'openChannels',
703
+ ({fundChannels, fundingPsbt, getFunding, openChannels}, cbk) =>
704
+ {
705
+ const fundingError = getFunding.error || fundingPsbt.error;
706
+ const error = fundChannels.error || fundingError;
707
+
708
+ // Exit early when the opening had an error and broadcasting isn't safe
709
+ if (!!error || !!fundingError) {
710
+ return cbk();
711
+ }
712
+
713
+ const [, multiNodeOpening] = openChannels;
714
+
715
+ // Exit early when not opening in multi-node mode
716
+ if (!multiNodeOpening) {
717
+ return cbk();
718
+ }
719
+
720
+ return broadcastChainTransaction({
721
+ lnd: args.lnd,
722
+ transaction: getFunding.value.transaction,
723
+ },
724
+ (err, res) => {
725
+ if (!!err) {
726
+ return cbk(err);
727
+ }
728
+
729
+ args.logger.info({transaction: getFunding.value.transaction});
730
+
731
+ return cbk();
732
+ });
733
+ }],
734
+
550
735
  // Cancel pending if there is an error
551
736
  cancelPending: [
552
737
  'fundChannels',
@@ -565,15 +750,20 @@ module.exports = (args, cbk) => {
565
750
  }
566
751
 
567
752
  args.logger.info({
568
- canceling_pending_channels: openChannels.pending.map(n => n.id),
753
+ canceling_pending_channels: openChannels.map(({node, pending}) => ({
754
+ node,
755
+ ids: pending.map(n => n.id),
756
+ })),
569
757
  });
570
758
 
571
- // Cancel outstanding pending channels when there is an error
572
- return asyncEach(openChannels.pending, (channel, cbk) => {
573
- return cancelPendingChannel({id: channel.id, lnd: args.lnd}, () => {
574
- // Ignore errors when trying to cancel a pending channel
575
- return cbk();
576
- });
759
+ return asyncEach(openChannels, ({lnd, pending}, cbk) => {
760
+ return asyncEach(pending => ({id}, cbk) => {
761
+ return cancelPendingChannel({id, lnd}, err => {
762
+ // Ignore errors when trying to cancel a pending channel
763
+ return cbk();
764
+ });
765
+ },
766
+ cbk);
577
767
  },
578
768
  () => {
579
769
  // Return the original error that canceled the finalization
@@ -592,7 +782,7 @@ module.exports = (args, cbk) => {
592
782
  return cbk();
593
783
  }
594
784
 
595
- // Exit early when there is no UTXOs to unlock
785
+ // Exit early when there are no UTXOs to unlock, like external funding
596
786
  if (!isArray(getFunding.inputs)) {
597
787
  return cbk(cancelPending);
598
788
  }
@@ -619,28 +809,31 @@ module.exports = (args, cbk) => {
619
809
 
620
810
  // Set fee rates
621
811
  setFeeRates: [
622
- 'cancelPending',
812
+ 'broadcastChainTransaction',
813
+ 'cancelLocks',
623
814
  'detectFunding',
624
815
  'fundChannels',
625
- ({}, cbk) =>
816
+ 'getLnds',
817
+ 'opens',
818
+ ({getLnds, opens}, cbk) =>
626
819
  {
627
820
  // Exit early when not specifying fee rates
628
821
  if (args.set_fee_rates.length !== args.public_keys.length) {
629
822
  return cbk();
630
823
  }
631
824
 
632
- const feesToSet = args.set_fee_rates.map((rate, i) => ({
633
- rate,
634
- public_key: args.public_keys[i],
635
- }));
825
+ return asyncEachSeries(opens, ({channels, node}, cbk) => {
826
+ const {lnd} = getLnds.find(n => n.node === node);
636
827
 
637
- return asyncEachSeries(feesToSet, (toSet, cbk) => {
638
- return adjustFees({
639
- fee_rate: toSet.rate,
640
- fs: args.fs,
641
- lnd: args.lnd,
642
- logger: args.logger,
643
- to: [toSet.public_key],
828
+ return asyncEachSeries(channels, (channel, cbk) => {
829
+ return adjustFees({
830
+ lnd,
831
+ fee_rate: channel.rate,
832
+ fs: args.fs,
833
+ logger: args.logger,
834
+ to: [channel.partner_public_key],
835
+ },
836
+ cbk);
644
837
  },
645
838
  cbk);
646
839
  },
@@ -649,6 +842,7 @@ module.exports = (args, cbk) => {
649
842
 
650
843
  // Transaction complete
651
844
  completed: [
845
+ 'broadcastChainTransaction',
652
846
  'cancelPending',
653
847
  'fundingPsbt',
654
848
  'getFunding',
@@ -34,6 +34,7 @@ const {isMessageReplyAction} = require('ln-telegram');
34
34
  const {notifyOfForwards} = require('ln-telegram');
35
35
  const {postChainTransaction} = require('ln-telegram');
36
36
  const {postClosedMessage} = require('ln-telegram');
37
+ const {postClosingMessage} = require('ln-telegram');
37
38
  const {postCreatedTrade} = require('ln-telegram');
38
39
  const {postOpenMessage} = require('ln-telegram');
39
40
  const {postOpeningMessage} = require('ln-telegram');
@@ -684,6 +685,20 @@ module.exports = (args, cbk) => {
684
685
 
685
686
  subscriptions.push(sub);
686
687
 
688
+ // Listen for pending closing channel events
689
+ sub.on('closing', update => {
690
+ return postClosingMessage({
691
+ from,
692
+ lnd,
693
+ closing: update.channels,
694
+ id: connectedId,
695
+ nodes: getNodes,
696
+ send: (id, msg, opt) => bot.api.sendMessage(id, msg, opt),
697
+ },
698
+ err => !!err ? logger.error({from, closing_err: err}) : null);
699
+ });
700
+
701
+ // Listen for pending opening events
687
702
  sub.on('opening', update => {
688
703
  return postOpeningMessage({
689
704
  from,
@@ -752,7 +767,6 @@ module.exports = (args, cbk) => {
752
767
  return notifyOfForwards({
753
768
  from,
754
769
  lnd,
755
- request,
756
770
  forwards: res.forwards.filter(forward => {
757
771
  if (!limits || !limits.min_forward_tokens) {
758
772
  return true;
@@ -761,8 +775,9 @@ module.exports = (args, cbk) => {
761
775
  return forward.tokens >= limits.min_forward_tokens;
762
776
  }),
763
777
  id: connectedId,
764
- key: apiKey.key,
765
778
  node: node.public_key,
779
+ nodes: getNodes,
780
+ send: (id, msg, opt) => bot.api.sendMessage(id, msg, opt),
766
781
  },
767
782
  err => {
768
783
  if (!!err) {
@@ -845,10 +860,8 @@ module.exports = (args, cbk) => {
845
860
  }
846
861
 
847
862
  return postSettledPayment({
848
- request,
849
863
  from: node.from,
850
864
  id: connectedId,
851
- key: apiKey.key,
852
865
  lnd: node.lnd,
853
866
  nodes: getNodes.map(n => n.public_key),
854
867
  payment: {
@@ -857,10 +870,9 @@ module.exports = (args, cbk) => {
857
870
  safe_fee: payment.safe_fee,
858
871
  safe_tokens: payment.safe_tokens,
859
872
  },
873
+ send: (id, msg, opts) => bot.api.sendMessage(id, msg, opts),
860
874
  },
861
875
  err => !!err ? logger.error({post_payment_error: err}) : null);
862
-
863
- return;
864
876
  });
865
877
 
866
878
  sub.on('error', err => {
@@ -0,0 +1,113 @@
1
+ const {addPeer} = require('ln-service');
2
+ const asyncAuto = require('async/auto');
3
+ const asyncEach = require('async/each');
4
+ const asyncRetry = require('async/retry');
5
+ const {createChainAddress} = require('ln-service');
6
+ const {fundPsbt} = require('ln-service');
7
+ const {getChannels} = require('ln-service');
8
+ const {getPendingChannels} = require('ln-service');
9
+ const {getWalletInfo} = require('ln-service');
10
+ const {openChannel} = require('ln-service');
11
+ const {signPsbt} = require('ln-service');
12
+ const {spawnLightningCluster} = require('ln-docker-daemons');
13
+ const {test} = require('@alexbosworth/tap');
14
+ const {Transaction} = require('bitcoinjs-lib');
15
+
16
+ const {openChannels} = require('./../../peers');
17
+
18
+ const count = 100;
19
+ const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
20
+ const interval = 200;
21
+ const log = () => {};
22
+ const size = 2;
23
+ const times = 1000;
24
+
25
+ // Opening channels should open channels with specified nodes
26
+ test(`Open channels`, async ({end, equal, strictSame}) => {
27
+ const {kill, nodes} = await spawnLightningCluster({size});
28
+
29
+ const [{generate, lnd}, target] = nodes;
30
+
31
+ try {
32
+ await generate({count});
33
+
34
+ await asyncRetry({interval, times}, async () => {
35
+ await addPeer({lnd, public_key: target.id, socket: target.socket});
36
+
37
+ await asyncEach(nodes, async ({lnd}) => {
38
+ const chain = await getWalletInfo({lnd});
39
+
40
+ if (!chain.is_synced_to_chain || !chain.is_synced_to_graph) {
41
+ throw new Error('WaitingForSync');
42
+ }
43
+ });
44
+ });
45
+
46
+ const {address} = await createChainAddress({lnd});
47
+
48
+ await delay(4000);
49
+
50
+ // Open a single channel from a single node
51
+ await asyncAuto({
52
+ // Open channel
53
+ propose: async () => {
54
+ await openChannels({
55
+ lnd,
56
+ ask: async (args, cbk) => {
57
+ if (args.name === 'internal') {
58
+ return cbk({internal: false});
59
+ }
60
+
61
+ if (args.name === 'fund') {
62
+ const address = args.message.split(' ')[9];
63
+
64
+ const {psbt} = await fundPsbt({
65
+ lnd,
66
+ outputs: [{address, tokens: 6e6}],
67
+ });
68
+
69
+ const signed = await signPsbt({lnd, psbt});
70
+
71
+ return cbk({fund: signed.psbt});
72
+ }
73
+
74
+ throw new Error('UnrecognizedParameter');
75
+ },
76
+ capacities: ['6*m'],
77
+ cooperative_close_addresses: [address],
78
+ fs: {getFile: () => {}},
79
+ gives: [1e5],
80
+ logger: {info: log, error: log},
81
+ opening_nodes: [],
82
+ public_keys: [target.id],
83
+ request: () => {},
84
+ set_fee_rates: [],
85
+ types: [],
86
+ });
87
+ },
88
+
89
+ // Generate blocks until the channel confirms
90
+ generate: async () => {
91
+ return await asyncRetry({interval, times}, async () => {
92
+ await generate({});
93
+
94
+ const {channels} = await getChannels({lnd});
95
+
96
+ if (!channels.length) {
97
+ throw new Error('Expected Channels');
98
+ }
99
+
100
+ const [channel] = channels;
101
+
102
+ equal(channel.remote_balance, 1e5, 'Gift balance is reflected');
103
+ equal(channel.capacity, 6e6, 'Channel capacity is set');
104
+ equal(channel.cooperative_close_address, address, 'Coop address');
105
+ });
106
+ },
107
+ });
108
+ } catch (err) {
109
+ equal(err, null, 'Expected no error');
110
+ }
111
+
112
+ await kill({});
113
+ });
@@ -8,6 +8,8 @@ const makeArgs = overrides => {
8
8
  capacities: [2],
9
9
  gives: ['1'],
10
10
  nodes: [Buffer.alloc(33, 3).toString('hex')],
11
+ rates: [],
12
+ saved: [],
11
13
  types: ['private'],
12
14
  };
13
15
 
@@ -21,12 +23,16 @@ const tests = [
21
23
  args: makeArgs({}),
22
24
  description: 'Arguments are mapped to channel details',
23
25
  expected: {
24
- channels: [{
25
- capacity: 2,
26
- cooperative_close_address: 'address',
27
- give_tokens: 1,
28
- is_private: true,
29
- partner_public_key: Buffer.alloc(33, 3).toString('hex'),
26
+ opens: [{
27
+ channels: [{
28
+ capacity: 2,
29
+ cooperative_close_address: 'address',
30
+ give_tokens: 1,
31
+ is_private: true,
32
+ node: undefined,
33
+ partner_public_key: Buffer.alloc(33, 3).toString('hex'),
34
+ rate: undefined,
35
+ }],
30
36
  }],
31
37
  },
32
38
  },
@@ -39,15 +45,59 @@ const tests = [
39
45
  }),
40
46
  description: 'Remove optional arguments',
41
47
  expected: {
42
- channels: [{
43
- capacity: 5000000,
44
- cooperative_close_address: undefined,
45
- give_tokens: undefined,
46
- is_private: false,
47
- partner_public_key: Buffer.alloc(33, 3).toString('hex'),
48
+ opens: [{
49
+ channels: [{
50
+ capacity: 5000000,
51
+ cooperative_close_address: undefined,
52
+ give_tokens: undefined,
53
+ is_private: false,
54
+ node: undefined,
55
+ partner_public_key: Buffer.alloc(33, 3).toString('hex'),
56
+ rate: undefined,
57
+ }],
48
58
  }],
49
59
  },
50
60
  },
61
+ {
62
+ args: makeArgs({
63
+ addresses: ['coopCloseAddressNodeA', 'coopCloseAddressNodeB'],
64
+ capacities: [1, 2],
65
+ gives: [3, 4],
66
+ nodes: ['remoteNodeA', 'remoteNodeB'],
67
+ rates: ['1', '2'],
68
+ saved: ['savedA', 'savedB'],
69
+ types: ['private', 'public'],
70
+ }),
71
+ description: 'Two nodes are batch opening',
72
+ expected: {
73
+ opens: [
74
+ {
75
+ channels: [{
76
+ capacity: 1,
77
+ cooperative_close_address: 'coopCloseAddressNodeA',
78
+ give_tokens: 3,
79
+ is_private: true,
80
+ node: 'savedA',
81
+ partner_public_key: 'remoteNodeA',
82
+ rate: '1',
83
+ }],
84
+ node: 'savedA',
85
+ },
86
+ {
87
+ channels: [{
88
+ capacity: 2,
89
+ cooperative_close_address: 'coopCloseAddressNodeB',
90
+ give_tokens: 4,
91
+ is_private: false,
92
+ node: 'savedB',
93
+ partner_public_key: 'remoteNodeB',
94
+ rate: '2',
95
+ }],
96
+ node: 'savedB',
97
+ },
98
+ ],
99
+ },
100
+ },
51
101
  ];
52
102
 
53
103
  tests.forEach(({args, description, error, expected}) => {