balanceofsatoshis 11.36.3 → 11.40.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.40.0
4
+
5
+ - `peers`: Add `DISK_USAGE_MB` filter to `--filter` formulas for est disk usage
6
+ - `peers`: Add `est_disk_usage_mb` to allowed `--sort` fields
7
+
8
+ ## 11.39.0
9
+
10
+ - `limit-forwarding`: Add new command to limit allowed routing
11
+
12
+ ## 11.38.0
13
+
14
+ - `advertise`: Add support for `--max-hops` to specify a maximum graph distance
15
+ - `advertise`: Add support for `--min-hops` to specify a minimum graph distance
16
+
17
+ ## 11.37.0
18
+
19
+ - `advertise`: Add support for specifying a maximum ad spend with `--budget`
20
+
3
21
  ## 11.36.3
4
22
 
5
23
  - `telegram`: Add support for `--use-proxy` to specify a SOCKS proxy server
package/bos CHANGED
@@ -98,19 +98,27 @@ 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')
103
+ .option('--budget <budget>', 'Spending amount to allow for advertising', INT)
101
104
  .option('--dryrun', 'Avoid actually sending advertisements')
102
105
  .option('--filter <expression>', 'Require node match condition', REPEATABLE)
106
+ .option('--max-hops <max_hops>', 'Maximum number of relaying nodes', INT)
103
107
  .option('--message <message>', 'Custom advertisement message')
108
+ .option('--min-hops <min_hops>', 'Minimum number of relaying nodes', INT)
104
109
  .option('--node <node_name>', 'Advertise via saved node')
105
110
  .action((args, options, logger) => {
106
111
  return new Promise(async (resolve, reject) => {
107
112
  try {
108
113
  return await services.advertise({
109
114
  logger,
115
+ budget: options.budget || undefined,
110
116
  filters: flatten([options.filter].filter(n => !!n)),
111
117
  is_dry_run: !!options.dryrun,
112
118
  lnd: (await lndForNode(logger, options.node)).lnd,
113
119
  message: options.message,
120
+ max_hops: options.maxHops,
121
+ min_hops: options.minHops,
114
122
  });
115
123
  } catch (err) {
116
124
  return reject(logger.error({err}));
@@ -816,6 +824,28 @@ prog
816
824
  });
817
825
  })
818
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
+
819
849
  // Get inbound liquidity information: available inbound off-chain tokens
820
850
  .command('inbound-liquidity', 'Get inbound liquidity size')
821
851
  .option('--above <tokens>', 'Return amount above watermark', INT)
@@ -1138,8 +1168,11 @@ prog
1138
1168
  .help('Icons: 🤢 often d/c, 💸 active HTLC, 💀 d/c, 🌚 private')
1139
1169
  .help('Icons: 🧊 delayed coop close, ⏳ pending channel, 🚫 in disabled')
1140
1170
  .help('Icons: 🦐 limited max htlc')
1141
- .help('Filters can take formula expressions like inbound_liquidity > 1*m')
1142
- .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"')
1143
1176
  .option('--active', 'Only active peer channels')
1144
1177
  .option('--complete', 'Show complete set of records in non table view')
1145
1178
  .option('--fee-days <past_days>', 'Include fees earned over n days', INT)
@@ -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",
@@ -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 || ' ',
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.36.3"
84
+ "version": "11.40.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);
158
174
 
159
- return cbk(null, isRoutingPossible);
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
+ }
184
+
185
+ // There exists a route to the destination within range constraints
186
+ return cbk(null, true);
160
187
  });
161
188
  },
162
189
  cbk);
@@ -164,11 +191,22 @@ module.exports = (args, cbk) => {
164
191
 
165
192
  // Send message to nodes
166
193
  send: ['message', 'nodes', ({message, nodes}, cbk) => {
194
+ const maxSpendPerNode = sendTokens + maxFeeTokens;
167
195
  const sent = [];
168
196
 
169
197
  args.logger.info({routable_nodes: nodes.length});
170
198
 
171
199
  return asyncMapSeries(nodes, (node, cbk) => {
200
+ const paidTokens = ceil(Number(sent.reduce((sum, n) => {
201
+ return sum + BigInt(n.mtokens) + BigInt(n.fee_mtokens);
202
+ },
203
+ BigInt(Number()))) / mtokPerToken);
204
+
205
+ // Check that the potential next ad would not go over budget
206
+ if (!!args.budget && paidTokens + maxSpendPerNode > args.budget) {
207
+ return cbk([400, 'AdvertisingBudgetExhausted']);
208
+ }
209
+
172
210
  const probe = subscribeToProbeForRoute({
173
211
  cltv_delay: cltvDelay,
174
212
  destination: node.public_key,
@@ -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,