balanceofsatoshis 23.1.8 → 23.1.10

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,14 @@
1
1
  # Versions
2
2
 
3
+ ## 23.1.10
4
+
5
+ - Add support for LND 0.21.3
6
+ - Add support for LND 0.20.4
7
+
8
+ ## 23.1.9
9
+
10
+ - `rebalance`: Apply `--in-filter` and `--out-filter` to key and alias peers
11
+
3
12
  ## 23.1.8
4
13
 
5
14
  - Add support for LND 0.21.2
package/README.md CHANGED
@@ -7,8 +7,8 @@ Commands for working with LND balances.
7
7
 
8
8
  Supported LND versions:
9
9
 
10
- - v0.21.0-beta to v0.21.2-beta
11
- - v0.20.0-beta to v0.20.3-beta
10
+ - v0.21.0-beta to v0.21.3-beta
11
+ - v0.20.0-beta to v0.20.4-beta
12
12
 
13
13
  ## Install
14
14
 
package/bos CHANGED
@@ -1735,7 +1735,7 @@ prog
1735
1735
  .option('--avoid-high-fee-routes', 'Avoid trying routes above max-fee-rate')
1736
1736
  .option('--avoid-list', 'Path to file with newline separated list of avoids')
1737
1737
  .option('--in <pubkey_or_alias>', 'Route in through a specific peer')
1738
- .option('--in-filter <in_filter>', 'Filter inbound tag nodes', REPEATABLE)
1738
+ .option('--in-filter <in_filter>', 'Filter inbound peer/tag', REPEATABLE)
1739
1739
  .option('--in-target-outbound <amt>', 'Balance up to outbound amount')
1740
1740
  .option('--max-fee <max_fee>', 'Maximum fee to pay')
1741
1741
  .option('--max-fee-rate <max_fee_rate>', 'Max fee rate to pay')
@@ -1743,7 +1743,7 @@ prog
1743
1743
  .option('--no-color', 'Mute all colors')
1744
1744
  .option('--node <node_name>', 'Saved node to use for rebalance')
1745
1745
  .option('--out <pubkey_or_alias>', 'Route out through a specific peer')
1746
- .option('--out-filter <out_filter>', 'Filter outbound tag nodes', REPEATABLE)
1746
+ .option('--out-filter <out_filter>', 'Filter outbound peer/tag', REPEATABLE)
1747
1747
  .option('--out-target-inbound <amount>', 'Balance up to inbound amount')
1748
1748
  .action((args, options, logger) => {
1749
1749
  return new Promise(async (resolve, reject) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "allowScripts": {
3
- "protobufjs@7.6.5": true,
3
+ "protobufjs@7.6.6": true,
4
4
  "ssh2@1.17.0": true,
5
5
  "cpu-features@0.0.10": true
6
6
  },
@@ -15,7 +15,7 @@
15
15
  "url": "https://github.com/alexbosworth/balanceofsatoshis/issues"
16
16
  },
17
17
  "dependencies": {
18
- "@alexbosworth/blockchain": "4.5.1",
18
+ "@alexbosworth/blockchain": "4.7.2",
19
19
  "@alexbosworth/caporal": "2.0.0",
20
20
  "@alexbosworth/fiat": "2.0.0",
21
21
  "@alexbosworth/formulas": "1.4.0",
@@ -35,13 +35,13 @@
35
35
  "colorette": "2.0.20",
36
36
  "csv-parse": "7.0.2",
37
37
  "goldengate": "16.0.6",
38
- "grammy": "1.45.1",
38
+ "grammy": "1.46.0",
39
39
  "import-lazy": "4.0.0",
40
40
  "ini": "7.0.0",
41
- "inquirer": "14.1.0",
41
+ "inquirer": "14.2.0",
42
42
  "ln-accounting": "10.0.5",
43
- "ln-service": "59.3.4",
44
- "ln-sync": "8.0.5",
43
+ "ln-service": "59.3.7",
44
+ "ln-sync": "8.0.6",
45
45
  "ln-telegram": "8.0.5",
46
46
  "moment": "2.30.1",
47
47
  "paid-services": "8.0.6",
@@ -55,7 +55,7 @@
55
55
  },
56
56
  "description": "Lightning balance CLI",
57
57
  "devDependencies": {
58
- "invoices": "6.1.3",
58
+ "invoices": "6.1.4",
59
59
  "ln-docker-daemons": "8.0.4"
60
60
  },
61
61
  "engines": {
@@ -83,5 +83,5 @@
83
83
  "postpublish": "docker buildx build --platform linux/amd64,linux/arm64 -t alexbosworth/balanceofsatoshis -t alexbosworth/balanceofsatoshis:$npm_package_version --push .",
84
84
  "test": "node --experimental-test-coverage --test 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/telegram/*.js test/wallets/*.js"
85
85
  },
86
- "version": "23.1.8"
86
+ "version": "23.1.10"
87
87
  }
@@ -0,0 +1,109 @@
1
+ const {decodeChanId} = require('bolt07');
2
+
3
+ const {shuffle} = require('./../arrays');
4
+ const {isMatchingFilters} = require('./../display');
5
+
6
+ const channelHeight = channel => decodeChanId({channel}).block_height;
7
+ const {max} = Math;
8
+ const sumOf = arr => arr.reduce((sum, n) => sum + n, Number());
9
+ const tok = n => Number(BigInt(n) / BigInt(1e3));
10
+ const uniq = arr => Array.from(new Set(arr));
11
+
12
+ /** Find a peer that matches filters
13
+
14
+ {
15
+ channels: [{
16
+ capacity: <Channel Token Capacity Number>
17
+ id: <Standard Format Channel Id String>
18
+ local_balance: <Channel Local Balance Tokens Number>
19
+ partner_public_key: <Peer Public Key Hex String>
20
+ pending_payments: [<Pending Payment Object>]
21
+ remote_balance: <Channel Remote Balance Tokens Number>
22
+ }]
23
+ [filters]: [<Filter Expression String>]
24
+ policies: [{
25
+ [base_fee_mtokens]: <Remote Base Fee Charged In Millitokens Number>
26
+ [fee_rate]: <Remote Fees Charged in Millitokens Per Million Number>
27
+ [is_disabled]: <Remote Channel Forwarding Is Disabled Bool>
28
+ public_key: <Remote Public Key Hex String>
29
+ }]
30
+ nodes: [<Candidate Peer Public Key Hex String>]
31
+ }
32
+
33
+ @returns
34
+ {
35
+ [failure]: {
36
+ error: <Error String>
37
+ formula: <Errored Formula String>
38
+ }
39
+ [match]: <Matching Peer Public Key Hex String>
40
+ }
41
+ */
42
+ module.exports = ({channels, filters, nodes, policies}) => {
43
+ const disabled = policies.filter(n => n.is_disabled).map(n => n.public_key);
44
+
45
+ const peers = channels.map(n => n.partner_public_key)
46
+ .filter(n => !disabled.includes(n));
47
+
48
+ const candidates = uniq((nodes || [])
49
+ .filter(n => peers.includes(n)));
50
+
51
+ // Filter out peers that do not fulfill the supplied criteria
52
+ const candidateResults = candidates
53
+ .map(key => {
54
+ if (!filters || !filters.length) {
55
+ return {match: key};
56
+ }
57
+
58
+ const peerPolicies = policies.filter(n => n.public_key === key);
59
+ const withPeer = channels.filter(n => n.partner_public_key === key);
60
+
61
+ const feeRates = peerPolicies.filter(n => n.fee_rate !== undefined);
62
+ const pendingPayments = withPeer.map(n => n.pending_payments.length);
63
+
64
+ const maxBaseFee = max(...feeRates.map(n => tok(n.base_fee_mtokens)));
65
+ const maxFeeRate = max(...feeRates.map(n => n.fee_rate));
66
+
67
+ const variables = {
68
+ capacity: sumOf(withPeer.map(n => n.capacity)),
69
+ heights: withPeer.map(n => channelHeight(n.id)),
70
+ inbound_liquidity: sumOf(withPeer.map(n => n.remote_balance)),
71
+ outbound_liquidity: sumOf(withPeer.map(n => n.local_balance)),
72
+ pending_payments: sumOf(pendingPayments),
73
+ };
74
+
75
+ if (!!feeRates.length) {
76
+ variables.inbound_base_fee = maxBaseFee;
77
+ variables.inbound_fee_rate = maxFeeRate;
78
+ }
79
+
80
+ const matching = isMatchingFilters({variables, filters: filters || []});
81
+
82
+ if (!!matching.failure) {
83
+ return matching;
84
+ }
85
+
86
+ if (!matching.is_matching) {
87
+ return;
88
+ }
89
+
90
+ return {match: key};
91
+ })
92
+ .filter(n => !!n);
93
+
94
+ // Exit early when there is no match
95
+ if (!candidateResults.length) {
96
+ return {};
97
+ }
98
+
99
+ // Exit early when there is a failure in a candidate
100
+ if (!!candidateResults.find(n => !!n.failure)) {
101
+ return candidateResults.find(n => !!n.failure);
102
+ }
103
+
104
+ const {shuffled} = shuffle({array: candidateResults});
105
+
106
+ const [{match}] = shuffled;
107
+
108
+ return {match};
109
+ };
@@ -1,12 +1,4 @@
1
- const {decodeChanId} = require('bolt07');
2
-
3
- const flatten = arr => [].concat(...arr);
4
- const {shuffle} = require('./../arrays');
5
- const {isMatchingFilters} = require('./../display');
6
-
7
- const {max} = Math;
8
- const sumOf = arr => arr.reduce((sum, n) => sum + n, Number());
9
- const tok = n => Number(BigInt(n) / BigInt(1e3));
1
+ const findPeerMatch = require('./find_peer_match');
10
2
 
11
3
  /** Find a node for a tag query
12
4
 
@@ -40,6 +32,7 @@ const tok = n => Number(BigInt(n) / BigInt(1e3));
40
32
  formula: <Errored Formula String>
41
33
  }
42
34
  [match]: <Matching Node Public Key Hex String>
35
+ [is_tag_filtered]: <Tag Matched But All Nodes Failed Filters Bool>
43
36
  [matches]: [{
44
37
  [alias]: <Tag Alias String>
45
38
  id: <Tag Id Hex String>
@@ -54,14 +47,8 @@ module.exports = ({channels, filters, policies, tags, query}) => {
54
47
  .map(n => n.partner_public_key)
55
48
  .filter(n => !disabled.includes(n));
56
49
 
57
- // Find tags that match on id or on alias, and also have relevant nodes
58
- const matches = tags.filter(tag => {
59
- const nodes = (tag.nodes || []).filter(n => peerKeys.includes(n));
60
-
61
- if (!nodes.length) {
62
- return false;
63
- }
64
-
50
+ // Find tags that match on id or on alias
51
+ const tagMatches = tags.filter(tag => {
65
52
  const alias = tag.alias || String();
66
53
 
67
54
  const isAliasMatch = alias.toLowerCase() === (query || '').toLowerCase();
@@ -70,9 +57,18 @@ module.exports = ({channels, filters, policies, tags, query}) => {
70
57
  return isAliasMatch || isIdMatch;
71
58
  });
72
59
 
60
+ // Limit tag matches to tags with relevant peers
61
+ const matches = tagMatches.filter(tag => {
62
+ return (tag.nodes || []).some(n => peerKeys.includes(n));
63
+ });
64
+
73
65
  const [tagMatch, ...otherTagMatches] = matches;
74
66
 
75
67
  // Exit early when there are no matches at all
68
+ if (!tagMatch && !!tagMatches.length && !!filters && !!filters.length) {
69
+ return {is_tag_filtered: true};
70
+ }
71
+
76
72
  if (!tagMatch) {
77
73
  return {};
78
74
  }
@@ -82,68 +78,16 @@ module.exports = ({channels, filters, policies, tags, query}) => {
82
78
  return {matches};
83
79
  }
84
80
 
85
- // Get the array of nodes in the tag match
86
- const tagMatches = tagMatch.nodes.filter(n => peerKeys.includes(n));
87
-
88
- // Filter out matches in the array of peers that do not fulfill criteria
89
- const array = tagMatches
90
- .map(key => {
91
- if (!filters || !filters.length) {
92
- return {match: key};
93
- }
94
-
95
- const peerPolicies = policies.filter(n => n.public_key === key);
96
- const withPeer = channels.filter(n => n.partner_public_key === key);
97
-
98
- const feeRates = peerPolicies.filter(n => n.fee_rate !== undefined);
99
- const pendingPayments = withPeer.map(n => n.pending_payments.length);
100
-
101
- const maxBaseFee = max(...feeRates.map(n => tok(n.base_fee_mtokens)));
102
- const maxFeeRate = max(...feeRates.map(n => n.fee_rate));
103
-
104
- const variables = {
105
- capacity: sumOf(withPeer.map(n => n.capacity)),
106
- heights: withPeer.map(n => {
107
- return decodeChanId({channel: n.id}).block_height;
108
- }),
109
- inbound_liquidity: sumOf(withPeer.map(n => n.remote_balance)),
110
- outbound_liquidity: sumOf(withPeer.map(n => n.local_balance)),
111
- pending_payments: sumOf(pendingPayments),
112
- };
113
-
114
- if (!!feeRates.length) {
115
- variables.inbound_base_fee = maxBaseFee;
116
- variables.inbound_fee_rate = maxFeeRate;
117
- }
118
-
119
- const matching = isMatchingFilters({variables, filters: filters || []});
120
-
121
- if (!!matching.failure) {
122
- return matching;
123
- }
124
-
125
- if (!matching.is_matching) {
126
- return;
127
- }
128
-
129
- return {match: key};
130
- })
131
- .filter(n => !!n);
132
-
133
- // Exit early when there is no match
134
- if (!array.length) {
135
- return {};
136
- }
81
+ const {failure, match} = findPeerMatch({
82
+ channels,
83
+ filters,
84
+ policies,
85
+ nodes: tagMatch.nodes.filter(n => peerKeys.includes(n)),
86
+ });
137
87
 
138
- // Exit early when there is a failure in the tag
139
- if (!!array.find(n => !!n.failure)) {
140
- return array.find(n => !!n.failure);
88
+ if (!failure && !match && !!filters && !!filters.length) {
89
+ return {is_tag_filtered: true};
141
90
  }
142
91
 
143
- // Shuffle the results
144
- const {shuffled} = shuffle({array});
145
-
146
- const [{match}] = shuffled;
147
-
148
- return {match};
92
+ return {failure, match};
149
93
  };
@@ -0,0 +1,149 @@
1
+ const asyncAuto = require('async/auto');
2
+ const {findKey} = require('ln-sync');
3
+ const {returnResult} = require('asyncjs-util');
4
+
5
+ const findPeerMatch = require('./../peers/find_peer_match');
6
+ const findTagMatch = require('./../peers/find_tag_match');
7
+
8
+ /** Find a peer for a rebalance
9
+
10
+ {
11
+ active_channels: [{
12
+ capacity: <Channel Token Capacity Number>
13
+ id: <Standard Format Channel Id String>
14
+ local_balance: <Channel Local Balance Tokens Number>
15
+ partner_public_key: <Peer Public Key Hex String>
16
+ pending_payments: [<Pending Payment Object>]
17
+ remote_balance: <Channel Remote Balance Tokens Number>
18
+ }]
19
+ channels: [{
20
+ partner_public_key: <Peer Public Key Hex String>
21
+ }]
22
+ direction: <In or Out Direction String>
23
+ [filters]: [<Peer Filter Formula String>]
24
+ lnd: <Authenticated LND API Object>
25
+ policies: [{
26
+ [base_fee_mtokens]: <Remote Base Fee Charged In Millitokens Number>
27
+ [fee_rate]: <Remote Fees Charged in Millitokens Per Million Number>
28
+ [is_disabled]: <Remote Channel Forwarding Is Disabled Bool>
29
+ public_key: <Remote Public Key Hex String>
30
+ }]
31
+ [query]: <Tag, Public Key, or Alias String>
32
+ tags: [{
33
+ [alias]: <Tag Alias String>
34
+ id: <Tag Id Hex String>
35
+ [nodes]: [<Public Key Hex String>]
36
+ }]
37
+ }
38
+
39
+ @returns via cbk or Promise
40
+ {
41
+ [public_key]: <Peer Public Key Hex String>
42
+ }
43
+ */
44
+ module.exports = (args, cbk) => {
45
+ return new Promise((resolve, reject) => {
46
+ return asyncAuto({
47
+ // Check arguments
48
+ validate: cbk => {
49
+ if (args.direction !== 'in' && args.direction !== 'out') {
50
+ return cbk([400, 'ExpectedInOrOutDirectionToFindRebalancePeer']);
51
+ }
52
+
53
+ return cbk();
54
+ },
55
+
56
+ // Find a tag, preserving tag precedence over peer aliases
57
+ tag: ['validate', ({}, cbk) => {
58
+ const filters = args.filters || [];
59
+
60
+ if (!args.query && !!filters.length) {
61
+ if (args.direction === 'in') {
62
+ return cbk([400, 'NoPeerMatchesFoundToSatisfyInboundFilter']);
63
+ }
64
+
65
+ return cbk([400, 'NoPeerMatchesFoundToSatisfyOutboundFilter']);
66
+ }
67
+
68
+ const {failure, is_tag_filtered, match, matches} = findTagMatch({
69
+ filters,
70
+ channels: args.active_channels,
71
+ policies: args.policies,
72
+ query: args.query,
73
+ tags: args.tags,
74
+ });
75
+
76
+ if (!!failure) {
77
+ return cbk([400, 'FailedToParseFilter', failure]);
78
+ }
79
+
80
+ if (!!matches) {
81
+ if (args.direction === 'in') {
82
+ return cbk([400, 'MultipleTagMatchesFoundForInPeer', {matches}]);
83
+ }
84
+
85
+ return cbk([400, 'MultipleTagMatchesFoundForOutPeer', {matches}]);
86
+ }
87
+
88
+ if (!!is_tag_filtered && args.direction === 'in') {
89
+ return cbk([400, 'NoPeerMatchesFoundToSatisfyInboundFilter']);
90
+ }
91
+
92
+ if (!!is_tag_filtered) {
93
+ return cbk([400, 'NoPeerMatchesFoundToSatisfyOutboundFilter']);
94
+ }
95
+
96
+ if (!!match) {
97
+ return cbk(null, {public_key: match});
98
+ }
99
+
100
+ return cbk();
101
+ }],
102
+
103
+ // Resolve a public key or node alias when the query was not a tag
104
+ key: ['tag', ({tag}, cbk) => {
105
+ if (!!tag) {
106
+ return cbk(null, tag);
107
+ }
108
+
109
+ return findKey({
110
+ lnd: args.lnd,
111
+ channels: args.channels,
112
+ query: args.query,
113
+ },
114
+ cbk);
115
+ }],
116
+
117
+ // Apply filters to the single explicitly resolved peer
118
+ peer: ['key', 'tag', ({key, tag}, cbk) => {
119
+ const filters = args.filters || [];
120
+
121
+ if (!!tag || !filters.length) {
122
+ return cbk(null, key);
123
+ }
124
+
125
+ const {failure, match} = findPeerMatch({
126
+ filters,
127
+ channels: args.active_channels,
128
+ nodes: [key.public_key.toLowerCase()],
129
+ policies: args.policies,
130
+ });
131
+
132
+ if (!!failure) {
133
+ return cbk([400, 'FailedToParseFilter', failure]);
134
+ }
135
+
136
+ if (!match && args.direction === 'in') {
137
+ return cbk([400, 'NoPeerMatchesFoundToSatisfyInboundFilter']);
138
+ }
139
+
140
+ if (!match) {
141
+ return cbk([400, 'NoPeerMatchesFoundToSatisfyOutboundFilter']);
142
+ }
143
+
144
+ return cbk(null, {public_key: match});
145
+ }],
146
+ },
147
+ returnResult({reject, resolve, of: 'peer'}, cbk));
148
+ });
149
+ };
@@ -3,7 +3,6 @@ const asyncMap = require('async/map');
3
3
  const asyncMapSeries = require('async/mapSeries');
4
4
  const asyncRetry = require('async/retry');
5
5
  const {createInvoice} = require('ln-service');
6
- const {findKey} = require('ln-sync');
7
6
  const {getChannel} = require('ln-service');
8
7
  const {getChannels} = require('ln-service');
9
8
  const {getHeight} = require('ln-service');
@@ -17,13 +16,14 @@ const {payViaRoutes} = require('ln-service');
17
16
  const {returnResult} = require('asyncjs-util');
18
17
  const {routeFromChannels} = require('ln-service');
19
18
 
20
- const {findTagMatch} = require('./../peers');
19
+ const findRebalancePeer = require('./find_rebalance_peer');
21
20
  const {formatFeeRate} = require('./../display');
22
21
  const {getIgnores} = require('./../routing');
23
22
  const {homePath} = require('../storage');
24
23
  const {probeDestination} = require('./../network');
25
24
  const {sortBy} = require('./../arrays');
26
25
 
26
+ const activeFilter = channels => channels.filter(n => !!n.is_active);
27
27
  const asRate = (fee, tokens) => ({rate: Math.ceil(fee / tokens * 1e6)});
28
28
  const {ceil} = Math;
29
29
  const channelMatch = /^\d*x\d*x\d*$/;
@@ -216,18 +216,22 @@ module.exports = (args, cbk) => {
216
216
  return getNode({lnd, public_key: getPublicKey.public_key}, cbk);
217
217
  }],
218
218
 
219
- // Find inbound tag public key
220
- findInTag: [
219
+ // Find inbound peer key
220
+ findInKey: [
221
221
  'getFees',
222
222
  'getInitialLiquidity',
223
223
  'getPublicKey',
224
224
  'getTags',
225
- ({getFees, getInitialLiquidity, getPublicKey, getTags}, cbk) =>
225
+ 'lnd',
226
+ ({getFees, getInitialLiquidity, getPublicKey, getTags, lnd}, cbk) =>
226
227
  {
227
228
  const id = getPublicKey.public_key;
228
229
 
229
- const {failure, match, matches} = findTagMatch({
230
- channels: getInitialLiquidity.channels.filter(n => n.is_active),
230
+ return findRebalancePeer({
231
+ lnd,
232
+ active_channels: activeFilter(getInitialLiquidity.channels),
233
+ channels: getInitialLiquidity.channels,
234
+ direction: 'in',
231
235
  filters: args.in_filters,
232
236
  policies: getFees.channels.map(channel => {
233
237
  const policy = channel.policies.find(n => n.public_key !== id);
@@ -239,41 +243,8 @@ module.exports = (args, cbk) => {
239
243
  public_key: policy.public_key,
240
244
  };
241
245
  }),
242
- tags: getTags.tags,
243
- query: args.in_through,
244
- });
245
-
246
- // Exit early when there is a filter error
247
- if (!!failure) {
248
- return cbk([400, 'FailedToParseFilter', failure]);
249
- }
250
-
251
- if (!!matches) {
252
- return cbk([400, 'MultipleTagMatchesFoundForInPeer', {matches}]);
253
- }
254
-
255
- if (!match && !!args.in_filters && !!args.in_filters.length) {
256
- return cbk([400, 'NoPeerMatchesFoundToSatisfyInboundFilter']);
257
- }
258
-
259
- return cbk(null, match);
260
- }],
261
-
262
- // Find inbound peer key if a name is specified
263
- findInKey: [
264
- 'findInTag',
265
- 'getInitialLiquidity',
266
- 'lnd',
267
- ({findInTag, getInitialLiquidity, lnd}, cbk) =>
268
- {
269
- if (!!findInTag) {
270
- return cbk(null, findInTag);
271
- }
272
-
273
- return findKey({
274
- lnd,
275
- channels: getInitialLiquidity.channels,
276
246
  query: args.in_through,
247
+ tags: getTags.tags,
277
248
  },
278
249
  (err, res) => {
279
250
  if (!!err) {
@@ -286,14 +257,12 @@ module.exports = (args, cbk) => {
286
257
 
287
258
  // Find outbound peer key if a name is specified
288
259
  findOutKey: [
289
- 'findInTag',
290
260
  'getFees',
291
261
  'getInitialLiquidity',
292
262
  'getPublicKey',
293
263
  'getTags',
294
264
  'lnd',
295
265
  ({
296
- findInTag,
297
266
  getFees,
298
267
  getInitialLiquidity,
299
268
  getPublicKey,
@@ -304,8 +273,11 @@ module.exports = (args, cbk) => {
304
273
  {
305
274
  const id = getPublicKey.public_key;
306
275
 
307
- const {failure, match, matches} = findTagMatch({
308
- channels: getInitialLiquidity.channels.filter(n => n.is_active),
276
+ return findRebalancePeer({
277
+ lnd,
278
+ active_channels: activeFilter(getInitialLiquidity.channels),
279
+ channels: getInitialLiquidity.channels,
280
+ direction: 'out',
309
281
  filters: args.out_filters,
310
282
  policies: getFees.channels.map(channel => {
311
283
  const policy = channel.policies.find(n => n.public_key !== id);
@@ -316,31 +288,8 @@ module.exports = (args, cbk) => {
316
288
  public_key: policy.public_key,
317
289
  };
318
290
  }),
319
- tags: getTags.tags,
320
- query: args.out_through,
321
- });
322
-
323
- // Exit early when there is a filter error
324
- if (!!failure) {
325
- return cbk([400, 'FailedToParseFilter', failure]);
326
- }
327
-
328
- if (!!matches) {
329
- return cbk([400, 'MultipleTagMatchesFoundForOutPeer', {matches}]);
330
- }
331
-
332
- if (match) {
333
- return cbk(null, match);
334
- }
335
-
336
- if (!match && !!args.out_filters && !!args.out_filters.length) {
337
- return cbk([400, 'NoPeerMatchesFoundToSatisfyOutboundFilter']);
338
- }
339
-
340
- return findKey({
341
- lnd,
342
- channels: getInitialLiquidity.channels,
343
291
  query: args.out_through,
292
+ tags: getTags.tags,
344
293
  },
345
294
  (err, res) => {
346
295
  if (!!err) {
@@ -371,8 +320,7 @@ module.exports = (args, cbk) => {
371
320
  {
372
321
  const ban = ignore.filter(n => !n.channel).map(n => n.from_public_key);
373
322
 
374
- const active = getInitialLiquidity.channels
375
- .filter(n => !!n.is_active)
323
+ const active = activeFilter(getInitialLiquidity.channels)
376
324
  .filter(n => !ban.includes(n.partner_public_key));
377
325
 
378
326
  const channels = active
@@ -435,8 +383,7 @@ module.exports = (args, cbk) => {
435
383
  const ban = ignore.filter(n => !n.channel).map(n => n.from_public_key);
436
384
  const hasInThrough = !!args.in_through;
437
385
 
438
- const activeChannels = getInitialLiquidity.channels
439
- .filter(n => !!n.is_active)
386
+ const activeChannels = activeFilter(getInitialLiquidity.channels)
440
387
  .filter(n => n.partner_public_key !== getOutbound.public_key)
441
388
  .filter(n => !ban.includes(n.partner_public_key));
442
389
 
@@ -31,7 +31,7 @@ const tests = [
31
31
  {
32
32
  args: makeArgs({}),
33
33
  description: 'A tagged node is found',
34
- expected: {match: Buffer.alloc(33, 3).toString('hex')},
34
+ expected: {failure: undefined, match: Buffer.alloc(33, 3).toString('hex')},
35
35
  },
36
36
  {
37
37
  args: makeArgs({filters: ['invalid formula']}),
@@ -41,8 +41,14 @@ const tests = [
41
41
  error: 'UnexpectedTrailingTokenForFormulaParsing',
42
42
  formula: 'invalid formula',
43
43
  },
44
+ match: undefined,
44
45
  },
45
46
  },
47
+ {
48
+ args: makeArgs({filters: ['outbound_liquidity > 1']}),
49
+ description: 'A tag with no peers satisfying a filter is identified',
50
+ expected: {is_tag_filtered: true},
51
+ },
46
52
  {
47
53
  args: makeArgs({
48
54
  channels: [
@@ -74,6 +80,7 @@ const tests = [
74
80
  }),
75
81
  description: 'A tagged node is found which complies with a filter',
76
82
  expected: {
83
+ failure: undefined,
77
84
  match: Buffer.alloc(33, 2).toString('hex'),
78
85
  },
79
86
  },
@@ -0,0 +1,329 @@
1
+ const {deepEqual} = require('node:assert').strict;
2
+ const {rejects} = require('node:assert').strict;
3
+ const test = require('node:test');
4
+
5
+ const findRebalancePeer = require('./../../swaps/find_rebalance_peer');
6
+
7
+ const highKey = Buffer.alloc(33, 2).toString('hex');
8
+ const lowKey = Buffer.alloc(33, 3).toString('hex');
9
+ const inactiveKey = Buffer.alloc(33, 4).toString('hex');
10
+ const unknownKey = Buffer.alloc(33, 5).toString('hex');
11
+ const inboundFilters = ['inbound_liquidity > 10'];
12
+
13
+ const channels = [
14
+ {
15
+ capacity: 10,
16
+ id: '1x1x1',
17
+ local_balance: 4,
18
+ partner_public_key: highKey,
19
+ pending_payments: [],
20
+ remote_balance: 6,
21
+ },
22
+ {
23
+ capacity: 10,
24
+ id: '2x2x2',
25
+ local_balance: 4,
26
+ partner_public_key: highKey,
27
+ pending_payments: [],
28
+ remote_balance: 6,
29
+ },
30
+ {
31
+ capacity: 10,
32
+ id: '3x3x3',
33
+ local_balance: 2,
34
+ partner_public_key: lowKey,
35
+ pending_payments: [],
36
+ remote_balance: 8,
37
+ },
38
+ ];
39
+
40
+ const makeLnd = aliases => ({
41
+ default: {
42
+ getNodeInfo: (args, cbk) => cbk(null, {
43
+ channels: [],
44
+ node: {
45
+ addresses: [],
46
+ alias: aliases[args.pub_key] || String(),
47
+ color: '#000000',
48
+ features: {},
49
+ last_update: 0,
50
+ pub_key: args.pub_key,
51
+ },
52
+ num_channels: 0,
53
+ total_capacity: '1',
54
+ }),
55
+ },
56
+ });
57
+
58
+ const makeArgs = overrides => {
59
+ const args = {
60
+ channels,
61
+ active_channels: channels,
62
+ direction: 'out',
63
+ filters: ['outbound_liquidity > 5'],
64
+ lnd: makeLnd({[highKey]: 'alpha', [lowKey]: 'beta'}),
65
+ policies: [],
66
+ query: highKey,
67
+ tags: [],
68
+ };
69
+
70
+ Object.keys(overrides).forEach(key => args[key] = overrides[key]);
71
+
72
+ return args;
73
+ };
74
+
75
+ const tests = [
76
+ {
77
+ args: makeArgs({direction: undefined}),
78
+ description: 'A known rebalance direction is required',
79
+ error: [400, 'ExpectedInOrOutDirectionToFindRebalancePeer'],
80
+ },
81
+ {
82
+ args: makeArgs({}),
83
+ description: 'An explicit public key satisfying the filter is selected',
84
+ expected: {public_key: highKey},
85
+ },
86
+ {
87
+ args: makeArgs({query: highKey.toUpperCase()}),
88
+ description: 'An uppercase public key is matched to the active peer',
89
+ expected: {public_key: highKey},
90
+ },
91
+ {
92
+ args: makeArgs({query: lowKey}),
93
+ description: 'An explicit public key failing the filter is rejected',
94
+ error: [400, 'NoPeerMatchesFoundToSatisfyOutboundFilter'],
95
+ },
96
+ {
97
+ args: makeArgs({query: 'alpha'}),
98
+ description: 'An explicit peer alias satisfying the filter is selected',
99
+ expected: {public_key: highKey},
100
+ },
101
+ {
102
+ args: makeArgs({query: 'beta'}),
103
+ description: 'Only the explicitly aliased peer is tested by the filter',
104
+ error: [400, 'NoPeerMatchesFoundToSatisfyOutboundFilter'],
105
+ },
106
+ {
107
+ args: makeArgs({
108
+ query: 'group',
109
+ tags: [{alias: 'group', id: '00', nodes: [highKey, lowKey]}],
110
+ }),
111
+ description: 'An outbound tag continues to select a filtered peer',
112
+ expected: {public_key: highKey},
113
+ },
114
+ {
115
+ args: makeArgs({
116
+ lnd: makeLnd({[highKey]: 'group'}),
117
+ query: 'group',
118
+ tags: [{alias: 'group', id: '00', nodes: [lowKey]}],
119
+ }),
120
+ description: 'A filtered-out tag does not fall through to a peer alias',
121
+ error: [400, 'NoPeerMatchesFoundToSatisfyOutboundFilter'],
122
+ },
123
+ {
124
+ args: makeArgs({
125
+ channels: channels.concat({partner_public_key: inactiveKey}),
126
+ lnd: makeLnd({[highKey]: 'inactive-tag'}),
127
+ query: 'inactive-tag',
128
+ tags: [{alias: 'inactive-tag', id: '00', nodes: [inactiveKey]}],
129
+ }),
130
+ description: 'A tag with no active peers does not fall through to an alias',
131
+ error: [400, 'NoPeerMatchesFoundToSatisfyOutboundFilter'],
132
+ },
133
+ {
134
+ args: makeArgs({
135
+ query: 'group',
136
+ tags: [
137
+ {alias: 'group', id: '00', nodes: [highKey]},
138
+ {alias: 'group', id: '11', nodes: [lowKey]},
139
+ ],
140
+ }),
141
+ description: 'Ambiguous outbound tags remain rejected',
142
+ error: [
143
+ 400,
144
+ 'MultipleTagMatchesFoundForOutPeer',
145
+ {
146
+ matches: [
147
+ {alias: 'group', id: '00', nodes: [highKey]},
148
+ {alias: 'group', id: '11', nodes: [lowKey]},
149
+ ],
150
+ },
151
+ ],
152
+ },
153
+ {
154
+ args: makeArgs({query: unknownKey}),
155
+ description: 'An unknown explicit public key does not satisfy a filter',
156
+ error: [400, 'NoPeerMatchesFoundToSatisfyOutboundFilter'],
157
+ },
158
+ {
159
+ args: makeArgs({
160
+ channels: channels.concat({partner_public_key: inactiveKey}),
161
+ lnd: makeLnd({[inactiveKey]: 'inactive'}),
162
+ query: 'inactive',
163
+ }),
164
+ description: 'An inactive aliased peer does not satisfy a filter',
165
+ error: [400, 'NoPeerMatchesFoundToSatisfyOutboundFilter'],
166
+ },
167
+ {
168
+ args: makeArgs({
169
+ filters: ['invalid formula'],
170
+ query: 'alpha',
171
+ }),
172
+ description: 'An invalid filter on an explicit alias is rejected',
173
+ error: [
174
+ 400,
175
+ 'FailedToParseFilter',
176
+ {
177
+ error: 'UnexpectedTrailingTokenForFormulaParsing',
178
+ formula: 'invalid formula',
179
+ },
180
+ ],
181
+ },
182
+ {
183
+ args: makeArgs({filters: [], query: 'beta'}),
184
+ description: 'An explicit alias is preserved when there are no filters',
185
+ expected: {public_key: lowKey},
186
+ },
187
+ {
188
+ args: makeArgs({
189
+ channels: channels.concat({partner_public_key: inactiveKey}),
190
+ filters: [],
191
+ lnd: makeLnd({[inactiveKey]: 'inactive'}),
192
+ query: 'inactive',
193
+ }),
194
+ description: 'An inactive alias still resolves when there are no filters',
195
+ expected: {public_key: inactiveKey},
196
+ },
197
+ {
198
+ args: makeArgs({query: undefined}),
199
+ description: 'An outbound filter still requires an outbound selector',
200
+ error: [400, 'NoPeerMatchesFoundToSatisfyOutboundFilter'],
201
+ },
202
+ {
203
+ args: makeArgs({filters: [], query: undefined}),
204
+ description: 'No selector and no filter preserves automatic selection',
205
+ expected: {public_key: undefined},
206
+ },
207
+ {
208
+ args: makeArgs({
209
+ direction: 'in',
210
+ filters: inboundFilters,
211
+ query: highKey,
212
+ }),
213
+ description: 'An inbound public key satisfying the filter is selected',
214
+ expected: {public_key: highKey},
215
+ },
216
+ {
217
+ args: makeArgs({
218
+ direction: 'in',
219
+ filters: inboundFilters,
220
+ query: lowKey,
221
+ }),
222
+ description: 'An inbound public key failing the filter is rejected',
223
+ error: [400, 'NoPeerMatchesFoundToSatisfyInboundFilter'],
224
+ },
225
+ {
226
+ args: makeArgs({
227
+ direction: 'in',
228
+ filters: inboundFilters,
229
+ query: 'alpha',
230
+ }),
231
+ description: 'An inbound alias satisfying the filter is selected',
232
+ expected: {public_key: highKey},
233
+ },
234
+ {
235
+ args: makeArgs({
236
+ direction: 'in',
237
+ filters: inboundFilters,
238
+ query: 'beta',
239
+ }),
240
+ description: 'Only the explicitly aliased inbound peer is filtered',
241
+ error: [400, 'NoPeerMatchesFoundToSatisfyInboundFilter'],
242
+ },
243
+ {
244
+ args: makeArgs({
245
+ direction: 'in',
246
+ filters: inboundFilters,
247
+ query: 'group',
248
+ tags: [{alias: 'group', id: '00', nodes: [highKey, lowKey]}],
249
+ }),
250
+ description: 'An inbound tag continues to select a filtered peer',
251
+ expected: {public_key: highKey},
252
+ },
253
+ {
254
+ args: makeArgs({
255
+ direction: 'in',
256
+ filters: inboundFilters,
257
+ lnd: makeLnd({[highKey]: 'group'}),
258
+ query: 'group',
259
+ tags: [{alias: 'group', id: '00', nodes: [lowKey]}],
260
+ }),
261
+ description: 'A filtered inbound tag does not fall through to an alias',
262
+ error: [400, 'NoPeerMatchesFoundToSatisfyInboundFilter'],
263
+ },
264
+ {
265
+ args: makeArgs({
266
+ direction: 'in',
267
+ filters: inboundFilters,
268
+ policies: [{is_disabled: true, public_key: highKey}],
269
+ query: highKey,
270
+ }),
271
+ description: 'An inbound-disabled explicit peer fails the filter',
272
+ error: [400, 'NoPeerMatchesFoundToSatisfyInboundFilter'],
273
+ },
274
+ {
275
+ args: makeArgs({
276
+ direction: 'in',
277
+ filters: [],
278
+ policies: [{is_disabled: true, public_key: highKey}],
279
+ query: highKey,
280
+ }),
281
+ description: 'An inbound-disabled explicit peer resolves without filters',
282
+ expected: {public_key: highKey},
283
+ },
284
+ {
285
+ args: makeArgs({
286
+ direction: 'in',
287
+ filters: inboundFilters,
288
+ query: 'group',
289
+ tags: [
290
+ {alias: 'group', id: '00', nodes: [highKey]},
291
+ {alias: 'group', id: '11', nodes: [lowKey]},
292
+ ],
293
+ }),
294
+ description: 'Ambiguous inbound tags remain rejected',
295
+ error: [
296
+ 400,
297
+ 'MultipleTagMatchesFoundForInPeer',
298
+ {
299
+ matches: [
300
+ {alias: 'group', id: '00', nodes: [highKey]},
301
+ {alias: 'group', id: '11', nodes: [lowKey]},
302
+ ],
303
+ },
304
+ ],
305
+ },
306
+ {
307
+ args: makeArgs({
308
+ direction: 'in',
309
+ filters: inboundFilters,
310
+ query: undefined,
311
+ }),
312
+ description: 'An inbound filter still requires an inbound selector',
313
+ error: [400, 'NoPeerMatchesFoundToSatisfyInboundFilter'],
314
+ },
315
+ ];
316
+
317
+ tests.forEach(({args, description, error, expected}) => {
318
+ return test(description, async () => {
319
+ if (!!error) {
320
+ await rejects(findRebalancePeer(args), error, 'Got expected error');
321
+ } else {
322
+ const res = await findRebalancePeer(args);
323
+
324
+ deepEqual(res, expected, 'Got expected rebalance peer');
325
+ }
326
+
327
+ return;
328
+ });
329
+ });