balanceofsatoshis 11.37.0 → 11.41.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,23 @@
1
1
  # Versions
2
2
 
3
+ ## 11.41.0
4
+
5
+ - `remove-peer`: Add interactive mode to select channels to close
6
+
7
+ ## 11.40.0
8
+
9
+ - `peers`: Add `DISK_USAGE_MB` filter to `--filter` formulas for est disk usage
10
+ - `peers`: Add `est_disk_usage_mb` to allowed `--sort` fields
11
+
12
+ ## 11.39.0
13
+
14
+ - `limit-forwarding`: Add new command to limit allowed routing
15
+
16
+ ## 11.38.0
17
+
18
+ - `advertise`: Add support for `--max-hops` to specify a maximum graph distance
19
+ - `advertise`: Add support for `--min-hops` to specify a minimum graph distance
20
+
3
21
  ## 11.37.0
4
22
 
5
23
  - `advertise`: Add support for specifying a maximum ad spend with `--budget`
package/bos CHANGED
@@ -98,10 +98,14 @@ prog
98
98
  .help('use --filter conditions to limit broadcast scope: capacity > 1*m')
99
99
  .help('--filter variables: CAPACITY/CHANNELS_COUNT/K/M')
100
100
  .help('Default filter scope: channels_count > 9')
101
+ .help('To only advertise to direct peers use --max-hops 0')
102
+ .help('To avoid advertising to direct peers use --min-hops 1')
101
103
  .option('--budget <budget>', 'Spending amount to allow for advertising', INT)
102
104
  .option('--dryrun', 'Avoid actually sending advertisements')
103
105
  .option('--filter <expression>', 'Require node match condition', REPEATABLE)
106
+ .option('--max-hops <max_hops>', 'Maximum number of relaying nodes', INT)
104
107
  .option('--message <message>', 'Custom advertisement message')
108
+ .option('--min-hops <min_hops>', 'Minimum number of relaying nodes', INT)
105
109
  .option('--node <node_name>', 'Advertise via saved node')
106
110
  .action((args, options, logger) => {
107
111
  return new Promise(async (resolve, reject) => {
@@ -113,6 +117,8 @@ prog
113
117
  is_dry_run: !!options.dryrun,
114
118
  lnd: (await lndForNode(logger, options.node)).lnd,
115
119
  message: options.message,
120
+ max_hops: options.maxHops,
121
+ min_hops: options.minHops,
116
122
  });
117
123
  } catch (err) {
118
124
  return reject(logger.error({err}));
@@ -818,6 +824,28 @@ prog
818
824
  });
819
825
  })
820
826
 
827
+ // Intercept forwarding requests, enforce requirements on acceptance
828
+ .command('limit-forwarding', 'Enforce rules for routing payments')
829
+ .option('--node <node_name>', 'Saved node to enforce rules on')
830
+ .option('--disable-forwards', 'Disable all forwards')
831
+ .option('--max-hours-since-last-block <h>', 'Require fresh blocks', INT, 5)
832
+ .option('--max-new-pending-per-hour <h>', 'Limit held HTLCs', INT)
833
+ .action((args, options, logger) => {
834
+ return new Promise(async (resolve, reject) => {
835
+ try {
836
+ return await peers.limitForwarding({
837
+ logger,
838
+ is_disabling_all_forwards: options.disableForwards || undefined,
839
+ lnd: (await lndForNode(logger, options.node)).lnd,
840
+ max_hours_since_last_block: options.maxHoursSinceLastBlock,
841
+ max_new_pending_per_hour: options.maxNewPendingPerHour,
842
+ });
843
+ } catch (err) {
844
+ return reject(logger.error({err}));
845
+ }
846
+ });
847
+ })
848
+
821
849
  // Get inbound liquidity information: available inbound off-chain tokens
822
850
  .command('inbound-liquidity', 'Get inbound liquidity size')
823
851
  .option('--above <tokens>', 'Return amount above watermark', INT)
@@ -1140,8 +1168,11 @@ prog
1140
1168
  .help('Icons: 🤢 often d/c, 💸 active HTLC, 💀 d/c, 🌚 private')
1141
1169
  .help('Icons: 🧊 delayed coop close, ⏳ pending channel, 🚫 in disabled')
1142
1170
  .help('Icons: 🦐 limited max htlc')
1143
- .help('Filters can take formula expressions like inbound_liquidity > 1*m')
1144
- .help('Filter formula variables: AGE|INBOUND_LIQUIDITY|OUTBOUND_LIQUIDITY')
1171
+ .help('Filters can take formula expressions to limit results')
1172
+ .help('Filter variable AGE: "age > 144 * 7" for peers older than a week')
1173
+ .help('Filter variable DISK_USAGE_MB: "disk_usage_mb > 9" for disk estimate')
1174
+ .help('Filter variable INBOUND_LIQUIDITY: "inbound_liquidity > 1*m"')
1175
+ .help('Filter variable OUTBOUND_LIQUIDITY: "outbound_liquidity > 1*m"')
1145
1176
  .option('--active', 'Only active peer channels')
1146
1177
  .option('--complete', 'Show complete set of records in non table view')
1147
1178
  .option('--fee-days <past_days>', 'Include fees earned over n days', INT)
@@ -1365,6 +1396,7 @@ prog
1365
1396
  .option('--outpoint', 'Only remove specific channel with funding txid:vout')
1366
1397
  .option('--private', 'Peer is privately connected')
1367
1398
  .option('--public', 'Peer is publicly connected')
1399
+ .option('--select-channels', 'Select channels to remove interactively')
1368
1400
  .action((args, options, logger) => {
1369
1401
  return new Promise(async (resolve, reject) => {
1370
1402
  try {
@@ -1374,6 +1406,7 @@ prog
1374
1406
  lnd,
1375
1407
  logger,
1376
1408
  address: options.address,
1409
+ ask: (n, cbk) => inquirer.prompt([n]).then(n => cbk(n)),
1377
1410
  chain_fee_rate: options.feeRate || undefined,
1378
1411
  fs: {getFile: readFile},
1379
1412
  idle_days: options.idleDays,
@@ -1384,6 +1417,7 @@ prog
1384
1417
  is_offline: !!options.offline,
1385
1418
  is_private: !!options.private,
1386
1419
  is_public: !!options.public,
1420
+ is_selecting_channels: options.selectChannels || undefined,
1387
1421
  omit: flatten([options.omit].filter(n => !!n)),
1388
1422
  outbound_liquidity_below: options.outboundBelow,
1389
1423
  outpoints: flatten([options.outpoint].filter(n => !!n)),
@@ -9,6 +9,7 @@
9
9
  },
10
10
  "peerSortOptions": [
11
11
  "alias",
12
+ "est_disk_usage_mb",
12
13
  "fee_earnings",
13
14
  "first_connected",
14
15
  "inbound_fee_rate",
@@ -236,6 +236,7 @@ module.exports = ({lnd, query}, cbk) => {
236
236
  .map(chan => {
237
237
  const [height] = chan.id.split('x');
238
238
  const local = formatTokens({tokens: chan.local_balance});
239
+ const pending = chan.pending_payments;
239
240
  const remote = formatTokens({tokens: chan.remote_balance});
240
241
 
241
242
  const inbound = `in: ${balance(remote)}`;
@@ -248,6 +249,7 @@ module.exports = ({lnd, query}, cbk) => {
248
249
  capacity: formatTokens({tokens: chan.capacity}).display,
249
250
  funding: `${chan.transaction_id} ${chan.transaction_vout}`,
250
251
  peer_created: chan.is_partner_initiated || undefined,
252
+ pending_payments: !!pending.length ? pending : undefined,
251
253
  };
252
254
  }),
253
255
  }
@@ -30,7 +30,9 @@ const {sortBy} = require('./../arrays');
30
30
  const closedSorts = ['fee_earnings', 'first_connected'];
31
31
  const defaultInvoicesLimit = 200;
32
32
  const defaultSort = 'first_connected';
33
+ const estimateDiskFootprint = n => Math.round(n * 500 / 1e6 * 10) / 10;
33
34
  const fromNow = epoch => !epoch ? undefined : moment(epoch * 1e3).fromNow();
35
+ const hasDiskFilter = filter => /disk_usage_mb/gim.test(filter);
34
36
  const {isArray} = Array;
35
37
  const {max} = Math;
36
38
  const minutesPerBlock = network => network === 'ltcmainnet' ? 10 / 4 : 10;
@@ -488,6 +490,8 @@ module.exports = (args, cbk) => {
488
490
  const disabled = policies.map(n => !!n.is_disabled).filter(n => !!n);
489
491
  const feeRate = !feeRates.length ? undefined : max(...feeRates);
490
492
  const maxHtlcSizes = policies.map(n => n.max_htlc_mtokens);
493
+ const pastStates = sumOf(active.map(n => n.past_states));
494
+
491
495
  const totalCapacity = sumOf(active.map(n => n.capacity));
492
496
 
493
497
  const totalMaxHtlc = mtokensAsTokens(sumOfBig(maxHtlcSizes));
@@ -503,6 +507,7 @@ module.exports = (args, cbk) => {
503
507
  filters: args.filters || [],
504
508
  variables: {
505
509
  age: blocks,
510
+ disk_usage_mb: estimateDiskFootprint(pastStates),
506
511
  fee_earnings: mtokensAsTokens(feeMtokens),
507
512
  inbound_fee_rate: feeRate,
508
513
  inbound_liquidity: sumOf(channels.map(n => n.remote_balance)),
@@ -539,6 +544,7 @@ module.exports = (args, cbk) => {
539
544
  return {
540
545
  alias: node.alias,
541
546
  downtime_percentage: round(100 * (downtime / (downtime + uptime))),
547
+ est_disk_usage_mb: estimateDiskFootprint(pastStates),
542
548
  fee_earnings: mtokensAsTokens(feeMtokens),
543
549
  first_connected: moment().subtract(blocks * mpb, 'minutes').unix(),
544
550
  inbound_fee_rate: feeRate,
@@ -590,6 +596,7 @@ module.exports = (args, cbk) => {
590
596
  return {
591
597
  alias: peer.alias,
592
598
  downtime_percentage: peer.downtime_percentage,
599
+ est_disk_usage_mb: peer.est_disk_usage_mb,
593
600
  fee_earnings: peer.fee_earnings,
594
601
  first_connected: fromNow(peer.first_connected),
595
602
  last_activity: fromNow(peer.last_activity),
@@ -615,6 +622,7 @@ module.exports = (args, cbk) => {
615
622
  return cbk(null, {
616
623
  peers: peers.peers.map(n => ({
617
624
  alias: n.alias,
625
+ est_disk_usage_mb: n.est_disk_usage_mb || undefined,
618
626
  fee_earnings: n.fee_earnings || undefined,
619
627
  downtime_percentage: n.downtime_percentage || undefined,
620
628
  first_connected: n.first_connected || undefined,
@@ -634,13 +642,18 @@ module.exports = (args, cbk) => {
634
642
  });
635
643
  }
636
644
 
645
+ const isDiskFilter = (args.filters || []).find(n => hasDiskFilter(n));
646
+ const isDiskSort = args.sort_by === 'est_disk_usage_mb';
637
647
  const isWideSize = !size || size.get().width > wideSizeCols;
638
648
 
649
+ const isShowingDisk = isDiskFilter || isDiskSort;
650
+
639
651
  return cbk(null, {
640
652
  peers: peers.peers,
641
653
  rows: []
642
654
  .concat([notNull([
643
655
  'Alias',
656
+ !!isShowingDisk ? 'Disk Mb' : null,
644
657
  'Inbound',
645
658
  'In Fee',
646
659
  'Outbound',
@@ -682,6 +695,7 @@ module.exports = (args, cbk) => {
682
695
 
683
696
  return notNull([
684
697
  alias.display,
698
+ !!isShowingDisk ? peer.est_disk_usage_mb || ' ' : null,
685
699
  inbound.display || ' ',
686
700
  peer.inbound_fee_rate || ' ',
687
701
  outbound.display || ' ',
@@ -1,6 +1,7 @@
1
1
  const asyncAuto = require('async/auto');
2
2
  const asyncEachSeries = require('async/eachSeries');
3
3
  const {closeChannel} = require('ln-service');
4
+ const {decodeChanId} = require('bolt07');
4
5
  const {getChainFeeRate} = require('ln-service');
5
6
  const {getChannels} = require('ln-service');
6
7
  const {getNetwork} = require('ln-sync');
@@ -11,20 +12,25 @@ const getPeers = require('./get_peers');
11
12
 
12
13
  const arrayWithEntries = arr => !!arr.length ? arr : undefined;
13
14
  const asOutpoint = n => `${n.transaction_id}:${n.transaction_vout}`;
15
+ const estimateDisk = n => Math.round(n * 500 / 1e6 * 10) / 10;
14
16
  const fastConf = 6;
15
17
  const {floor} = Math;
16
18
  const defaultDays = 365 * 2;
17
19
  const getMempoolRetries = 10;
20
+ const iconDisabled = channel => !channel.is_active ? '💀 ' : '';
21
+ const iconPending = channel => channel.pending_payments.length ? '💸 ' : ''
18
22
  const {isArray} = Array;
19
23
  const isPublicKey = n => !!n && /^0[2-3][0-9A-F]{64}$/i.test(n);
20
24
  const maxMempoolSize = 2e6;
21
25
  const regularConf = 72;
22
26
  const slowConf = 144;
27
+ const tokensAsBigUnit = tokens => (tokens / 1e8).toFixed(8);
23
28
 
24
29
  /** Close out channels with a peer and disconnect them
25
30
 
26
31
  {
27
32
  [address]: <Close Out Funds to On-Chain Address String>
33
+ ask: <Ask Function>
28
34
  [chain_fee_rate]: <Chain Fee Per VByte Number>
29
35
  fs: {
30
36
  getFile: <Read File Contents Function> (path, cbk) => {}
@@ -37,6 +43,7 @@ const slowConf = 144;
37
43
  [is_offline]: <Peer Is Disconnected Bool>
38
44
  [is_private]: <Peer is Privately Connected Bool>
39
45
  [is_public]: <Peer is Publicly Connected Bool>
46
+ [is_selecting_channels]: <Interactively Select Channels to Remove Bool>
40
47
  lnd: <Authenticated LND API Object>
41
48
  logger: <Winston Logger Object>
42
49
  [omit]: [<Avoid Peer With Public Key String>]
@@ -53,10 +60,18 @@ module.exports = (args, cbk) => {
53
60
  return asyncAuto({
54
61
  // Check arguments
55
62
  validate: cbk => {
63
+ if (!args.ask) {
64
+ return cbk([400, 'ExpectedAskFunctionToRemovePeer']);
65
+ }
66
+
56
67
  if (!args.fs) {
57
68
  return cbk([400, 'ExpectedFsMethodsToRemovePeer']);
58
69
  }
59
70
 
71
+ if (!!args.is_selecting_channels && !args.public_key) {
72
+ return cbk([400, 'ExpectedPeerToRemoveWhenSelectingChannels']);
73
+ }
74
+
60
75
  if (!args.lnd) {
61
76
  return cbk([400, 'LndIsRequiredToRemovePeer']);
62
77
  }
@@ -69,6 +84,10 @@ module.exports = (args, cbk) => {
69
84
  return cbk([400, 'ExpectedSpecificOutpointsToRemoveFromPeer']);
70
85
  }
71
86
 
87
+ if (!!args.outpoints.length && !args.public_key) {
88
+ return cbk([400, 'ExpectedPeerToRemoteWhenOutpointsSpecified']);
89
+ }
90
+
72
91
  if (!!args.public_key && !isPublicKey(args.public_key)) {
73
92
  return cbk([400, 'ExpectedPublicKeyOfPeerToRemove']);
74
93
  }
@@ -137,8 +156,94 @@ module.exports = (args, cbk) => {
137
156
  cbk);
138
157
  }],
139
158
 
159
+ // Determine outpoints to use
160
+ outpoints: ['getChannels', ({getChannels}, cbk) => {
161
+ // Exit early when a peer is not specified
162
+ if (!args.public_key) {
163
+ return cbk(null, []);
164
+ }
165
+
166
+ const channelsWithPeer = getChannels.channels
167
+ .filter(channel => {
168
+ // Ignore channels that are not the specified public key
169
+ if (channel.partner_public_key !== args.public_key) {
170
+ return false;
171
+ }
172
+
173
+ //Return channels with the peer
174
+ return true;
175
+ })
176
+ .sort((a, b) => {
177
+ const heightA = decodeChanId({channel: a.id}).block_height;
178
+ const heightB = decodeChanId({channel: b.id}).block_height;
179
+
180
+ // Sort channels by oldest to newest
181
+ return heightA - heightB;
182
+ });
183
+
184
+ // Exit early when no channels are available
185
+ if (!channelsWithPeer.length) {
186
+ return cbk([404, 'NoChannelsToCloseWithSpecifiedPeer']);
187
+ }
188
+
189
+ // Collect any outpoints that are unable to be cooperatively closed
190
+ const blocked = channelsWithPeer
191
+ .filter(channel => {
192
+ // Channels that are inactive or have HTLCs cannot be coop-closed
193
+ return !channel.is_active || !!channel.pending_payments.length;
194
+ })
195
+ .map(channel => asOutpoint(channel));
196
+
197
+ // Find a directly referenced outpoint that is in the blocked list
198
+ const blockedOutpoint = args.outpoints.find(n => blocked.includes(n));
199
+
200
+ // Make sure we aren't trying to coop close a channel that can't be
201
+ if (!args.is_forced && !!blockedOutpoint) {
202
+ return cbk([400, 'CannotCoopClose', {outpoint: blockedOutpoint}]);
203
+ }
204
+
205
+ // Exit early if not selecting a channel
206
+ if (!args.is_selecting_channels) {
207
+ return cbk(null, args.outpoints);
208
+ }
209
+
210
+ // Interactively select outpoints to close
211
+ return args.ask({
212
+ choices: channelsWithPeer.map(channel => {
213
+ // In closing, channels are identified by their funding outpoint
214
+ const value = asOutpoint(channel);
215
+
216
+ // Channels that are inactive or have HTLCs cannot be coop-closed
217
+ const isBlocked = blocked.includes(value);
218
+
219
+ const disk = `Est disk mb: ${estimateDisk(channel.past_states)}`;
220
+ const icon = iconDisabled(channel) || iconPending(channel);
221
+ const {id} = channel;
222
+ const inbound = `in: ${tokensAsBigUnit(channel.remote_balance)}`;
223
+ const outbound = `out: ${tokensAsBigUnit(channel.local_balance)}`;
224
+
225
+ return {
226
+ value,
227
+ checked: args.outpoints.includes(value),
228
+ disabled: !args.is_forced ? isBlocked : false,
229
+ name: `${icon}${id}: ${inbound} | ${outbound}. ${disk}.`,
230
+ };
231
+ }),
232
+ loop: false,
233
+ message: `Channels to ${!!args.is_forced ? 'force ' : ''}close?`,
234
+ name: 'outpoints',
235
+ type: 'checkbox',
236
+ validate: input => !!input.length,
237
+ },
238
+ ({outpoints}) => cbk(null, outpoints));
239
+ }],
240
+
140
241
  // Check channels for peer to make sure that they can be cleanly closed
141
- checkChannels: ['getChannels', ({getChannels}, cbk) => {
242
+ checkChannels: [
243
+ 'getChannels',
244
+ 'outpoints',
245
+ ({getChannels, outpoints}, cbk) =>
246
+ {
142
247
  // Exit early when a peer is not specified or force closing is OK
143
248
  if (!args.public_key || !!args.is_forced) {
144
249
  return cbk();
@@ -151,12 +256,12 @@ module.exports = (args, cbk) => {
151
256
  }
152
257
 
153
258
  // Exit early when there are no outpoints, consider all peer channels
154
- if (!args.outpoints.length) {
259
+ if (!outpoints.length) {
155
260
  return true;
156
261
  }
157
262
 
158
263
  // Only include selected channels
159
- return args.outpoints.includes(asOutpoint(channel));
264
+ return outpoints.includes(asOutpoint(channel));
160
265
  });
161
266
 
162
267
  const costToClose = selectedChannels
@@ -279,8 +384,9 @@ module.exports = (args, cbk) => {
279
384
  'checkChannels',
280
385
  'getChannels',
281
386
  'getNormalFee',
387
+ 'outpoints',
282
388
  'selectPeer',
283
- ({getChannels, getNormalFee, selectPeer}, cbk) =>
389
+ ({getChannels, getNormalFee, outpoints, selectPeer}, cbk) =>
284
390
  {
285
391
  // Exit early when there is no peer to close out with
286
392
  if (!selectPeer) {
@@ -292,11 +398,12 @@ module.exports = (args, cbk) => {
292
398
  const toClose = getChannels.channels
293
399
  .filter(chan => chan.partner_public_key === selectPeer.public_key)
294
400
  .filter(chan => {
295
- if (!args.outpoints.length) {
401
+ // When no outpoints are specified, all channels should be closed
402
+ if (!outpoints.length) {
296
403
  return true;
297
404
  }
298
405
 
299
- return args.outpoints.includes(asOutpoint(chan));
406
+ return !!outpoints.includes(asOutpoint(chan));
300
407
  });
301
408
 
302
409
  // Exit early when there are no channels to close
package/package.json CHANGED
@@ -36,7 +36,7 @@
36
36
  "invoices": "2.0.3",
37
37
  "ln-accounting": "5.0.5",
38
38
  "ln-service": "53.6.0",
39
- "ln-sync": "3.7.1",
39
+ "ln-sync": "3.8.0",
40
40
  "ln-telegram": "3.12.0",
41
41
  "moment": "2.29.1",
42
42
  "paid-services": "3.11.0",
@@ -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.37.0"
84
+ "version": "11.41.0"
85
85
  }
package/peers/index.js CHANGED
@@ -1,5 +1,11 @@
1
1
  const findTagMatch = require('./find_tag_match');
2
2
  const interceptInboundChannels = require('./intercept_inbound_channels');
3
+ const limitForwarding = require('./limit_forwarding');
3
4
  const openChannels = require('./open_channels');
4
5
 
5
- module.exports = {findTagMatch, interceptInboundChannels, openChannels};
6
+ module.exports = {
7
+ findTagMatch,
8
+ interceptInboundChannels,
9
+ limitForwarding,
10
+ openChannels,
11
+ };
@@ -0,0 +1,87 @@
1
+ const asyncAuto = require('async/auto');
2
+ const {enforceForwardRequestRules} = require('ln-sync');
3
+ const {returnResult} = require('asyncjs-util');
4
+
5
+ const disableAllForwards = 0;
6
+ const hoursAsSeconds = hours => hours * 60 * 60;
7
+
8
+ /** Limit forwarding requests
9
+
10
+ {
11
+ lnd: (await lndForNode(logger, options.node)).lnd,
12
+ logger: <Winston Logger Object>
13
+ [is_disabling_all_forwards]: <All Forwards Are Disabled Bool>
14
+ [max_hours_since_last_block]: options.maxHoursSinceLastBlock,
15
+ [max_new_pending_per_hour]: options.maxNewPendingPerHour,
16
+ }
17
+
18
+ @returns via cbk or Promise
19
+ */
20
+ module.exports = (args, cbk) => {
21
+ return new Promise((resolve, reject) => {
22
+ return asyncAuto({
23
+ // Check arguments
24
+ validate: cbk => {
25
+ if (!args.logger) {
26
+ return cbk([400, 'ExpectedWinstonLoggerToLimitForwarding']);
27
+ }
28
+
29
+ if (!args.lnd) {
30
+ return cbk([400, 'ExpectedAuthenticatedLndToLimitForwarding']);
31
+ }
32
+
33
+ return cbk();
34
+ },
35
+
36
+ // Max pending per hour
37
+ maxPendingPerHour: ['validate', ({}, cbk) => {
38
+ if (!!args.is_disabling_all_forwards) {
39
+ return cbk(null, disableAllForwards);
40
+ }
41
+
42
+ if (!!args.max_new_pending_per_hour) {
43
+ return cbk(null, args.max_new_pending_per_hour);
44
+ }
45
+
46
+ return cbk();
47
+ }],
48
+
49
+ // Max seconds allowed since the last block
50
+ maxSecondsSinceLastBlock: ['validate', ({}, cbk) => {
51
+ if (!args.max_hours_since_last_block) {
52
+ return cbk();
53
+ }
54
+
55
+ return cbk(null, hoursAsSeconds(args.max_hours_since_last_block));
56
+ }],
57
+
58
+ // Limit forward requests
59
+ limit: [
60
+ 'maxPendingPerHour',
61
+ 'maxSecondsSinceLastBlock',
62
+ ({maxPendingPerHour, maxSecondsSinceLastBlock}, cbk) =>
63
+ {
64
+ args.logger.info({limiting_forwards: true});
65
+
66
+ const sub = enforceForwardRequestRules({
67
+ lnd: args.lnd,
68
+ max_new_pending_per_hour: maxPendingPerHour,
69
+ max_seconds_since_last_block: maxSecondsSinceLastBlock,
70
+ });
71
+
72
+ sub.on('error', err => {
73
+ return cbk([503, 'UnexpectedErrorLimitingForwarding', {err}]);
74
+ });
75
+
76
+ sub.on('rejected', async rejected => {
77
+ const forward = `${rejected.in_channel} → ${rejected.out_channel}`;
78
+
79
+ const rejection = `${rejected.reject_reason} ${forward}`;
80
+
81
+ return args.logger.info({rejection});
82
+ });
83
+ }],
84
+ },
85
+ returnResult({reject, resolve}, cbk));
86
+ });
87
+ };
@@ -36,6 +36,7 @@ const mtokPerToken = 1e3;
36
36
  const pathTimeoutMs = 1000 * 45;
37
37
  const payTimeoutMs = 1000 * 60;
38
38
  const probeTimeoutMs = 1000 * 60 * 2;
39
+ const routeDistance = route => route.hops.length - 1;
39
40
  const sendTokens = 10;
40
41
  const sumOf = arr => arr.reduce((sum, n) => sum + n, Number());
41
42
  const textMessageType = '34349334';
@@ -49,6 +50,8 @@ const utf8AsHex = utf8 => Buffer.from(utf8, 'utf8').toString('hex');
49
50
  lnd: <Authenticated LND API Object>
50
51
  logger: <Winston Logger Object>
51
52
  [message]: <Message To Send String>
53
+ [max_hops]: <Maxmimum Relaying Nodes Number>
54
+ [min_hops]: <Minimum Relaying Nodes Number>
52
55
  }
53
56
  */
54
57
  module.exports = (args, cbk) => {
@@ -75,6 +78,14 @@ module.exports = (args, cbk) => {
75
78
  getGraph: ['validate', ({}, cbk) => {
76
79
  args.logger.info({sending_to_all_graph_nodes: true});
77
80
 
81
+ if (args.max_hops !== undefined) {
82
+ args.logger.info({maximum_relay_distance_allowed: args.max_hops});
83
+ }
84
+
85
+ if (args.min_hops !== undefined) {
86
+ args.logger.info({minimum_relay_distance_required: args.min_hops});
87
+ }
88
+
78
89
  return getNetworkGraph({lnd: args.lnd}, cbk);
79
90
  }],
80
91
 
@@ -154,9 +165,25 @@ module.exports = (args, cbk) => {
154
165
  tokens: sendTokens,
155
166
  },
156
167
  (err, res) => {
157
- const isRoutingPossible = !!res && !!res.route;
168
+ // Exit early when there is a problem getting a route
169
+ if (!!err || !res.route) {
170
+ return cbk(null, false);
171
+ }
172
+
173
+ const relaysCount = routeDistance(res.route);
174
+
175
+ // Exit early when there are too many relaying nodes
176
+ if (args.max_hops !== undefined && relaysCount > args.max_hops) {
177
+ return cbk(null, false);
178
+ }
179
+
180
+ // Exit early when there are too few relaying nodes
181
+ if (args.min_hops !== undefined && relaysCount < args.min_hops) {
182
+ return cbk(null, false);
183
+ }
158
184
 
159
- return cbk(null, isRoutingPossible);
185
+ // There exists a route to the destination within range constraints
186
+ return cbk(null, true);
160
187
  });
161
188
  },
162
189
  cbk);
@@ -43,7 +43,6 @@ const isPublicKey = n => /^[0-9A-F]{66}$/i.test(n);
43
43
  const legacyMaxRebalanceTokens = 4294967;
44
44
  const {max} = Math;
45
45
  const maxPaymentSize = 4294967;
46
- const maxRebalanceTokens = 16777215;
47
46
  const {min} = Math;
48
47
  const minInboundBalance = 4294967 * 2;
49
48
  const minRebalanceAmount = 5e4;
@@ -632,7 +631,7 @@ module.exports = (args, cbk) => {
632
631
  lnd,
633
632
  cltv_delta: defaultCltvDelta,
634
633
  description: 'Rebalance',
635
- tokens: min(maxRebalanceTokens, findRoute.route_maximum),
634
+ tokens: findRoute.route_maximum,
636
635
  },
637
636
  cbk);
638
637
  }],
@@ -170,6 +170,7 @@ const tests = [
170
170
  expected: {
171
171
  peers: [{
172
172
  alias: 'alias',
173
+ est_disk_usage_mb: undefined,
173
174
  fee_earnings: undefined,
174
175
  downtime_percentage: undefined,
175
176
  last_activity: undefined,
@@ -270,8 +271,9 @@ const tests = [
270
271
  expected: {
271
272
  peers: [{
272
273
  alias: 'alias',
273
- fee_earnings: undefined,
274
+ est_disk_usage_mb: undefined,
274
275
  downtime_percentage: undefined,
276
+ fee_earnings: undefined,
275
277
  last_activity: undefined,
276
278
  inbound_fee_rate: undefined,
277
279
  inbound_liquidity: 1,