balanceofsatoshis 23.1.7 → 23.1.9

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,6 +1,10 @@
1
1
  # Versions
2
2
 
3
- ## 23.1.7
3
+ ## 23.1.9
4
+
5
+ - `rebalance`: Apply `--in-filter` and `--out-filter` to key and alias peers
6
+
7
+ ## 23.1.8
4
8
 
5
9
  - Add support for LND 0.21.2
6
10
  - Add support for LND 0.20.3
package/bos CHANGED
@@ -4,6 +4,7 @@ const {lstat} = require('fs');
4
4
  const {mkdir} = require('fs');
5
5
  const {readdir} = require('fs');
6
6
  const {readFile} = require('fs');
7
+ const {rename} = require('fs');
7
8
  const {rmdir} = require('fs');
8
9
  const {spawn} = require('child_process');
9
10
  const {statSync} = require('fs');
@@ -1734,7 +1735,7 @@ prog
1734
1735
  .option('--avoid-high-fee-routes', 'Avoid trying routes above max-fee-rate')
1735
1736
  .option('--avoid-list', 'Path to file with newline separated list of avoids')
1736
1737
  .option('--in <pubkey_or_alias>', 'Route in through a specific peer')
1737
- .option('--in-filter <in_filter>', 'Filter inbound tag nodes', REPEATABLE)
1738
+ .option('--in-filter <in_filter>', 'Filter inbound peer/tag', REPEATABLE)
1738
1739
  .option('--in-target-outbound <amt>', 'Balance up to outbound amount')
1739
1740
  .option('--max-fee <max_fee>', 'Maximum fee to pay')
1740
1741
  .option('--max-fee-rate <max_fee_rate>', 'Max fee rate to pay')
@@ -1742,7 +1743,7 @@ prog
1742
1743
  .option('--no-color', 'Mute all colors')
1743
1744
  .option('--node <node_name>', 'Saved node to use for rebalance')
1744
1745
  .option('--out <pubkey_or_alias>', 'Route out through a specific peer')
1745
- .option('--out-filter <out_filter>', 'Filter outbound tag nodes', REPEATABLE)
1746
+ .option('--out-filter <out_filter>', 'Filter outbound peer/tag', REPEATABLE)
1746
1747
  .option('--out-target-inbound <amount>', 'Balance up to inbound amount')
1747
1748
  .action((args, options, logger) => {
1748
1749
  return new Promise(async (resolve, reject) => {
@@ -1752,7 +1753,7 @@ prog
1752
1753
  avoid: flatten([options.avoid].filter(n => !!n)),
1753
1754
  avoid_append: options.avoidAppend || undefined,
1754
1755
  avoid_list: options.avoidList || undefined,
1755
- fs: {appendFile, writeFile, getFile: readFile},
1756
+ fs: {appendFile, writeFile, getFile: readFile, renameFile: rename},
1756
1757
  in_filters: flatten([options.inFilter].filter(n => !!n)),
1757
1758
  in_outbound: options.inTargetOutbound || undefined,
1758
1759
  in_through: options.in || undefined,
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.5.3",
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.5",
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.7"
86
+ "version": "23.1.9"
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
+ };
@@ -0,0 +1,53 @@
1
+ const asyncAuto = require('async/auto');
2
+ const {returnResult} = require('asyncjs-util');
3
+
4
+ const matchNewLines = /\r?\n/;
5
+ const trim = n => n.trim();
6
+ const uniq = arr => Array.from(new Set(arr));
7
+
8
+ /** Get the list of avoid directives from an avoid list file
9
+
10
+ {
11
+ fs: {
12
+ getFile: <Read File Contents Function> (path, cbk) => {}
13
+ }
14
+ path: <Path To Avoid List File String>
15
+ }
16
+
17
+ @returns via cbk or Promise
18
+ {
19
+ lines: [<Avoid Directive Line String>]
20
+ }
21
+ */
22
+ module.exports = ({fs, path}, cbk) => {
23
+ return new Promise((resolve, reject) => {
24
+ return asyncAuto({
25
+ // Check arguments
26
+ validate: cbk => {
27
+ if (!fs) {
28
+ return cbk([400, 'ExpectedFilesystemMethodsToGetAvoidList']);
29
+ }
30
+
31
+ if (!path) {
32
+ return cbk([400, 'ExpectedPathToAvoidListFileToGetAvoidList']);
33
+ }
34
+
35
+ return cbk();
36
+ },
37
+
38
+ // Get the lines of the avoid list file
39
+ getList: ['validate', ({}, cbk) => {
40
+ return fs.getFile(path, (err, res) => {
41
+ if (!!err) {
42
+ return cbk([503, 'UnexpectedErrorGettingAvoidListFile', {err}]);
43
+ }
44
+
45
+ const lines = res.toString().split(matchNewLines).map(trim);
46
+
47
+ return cbk(null, {lines: uniq(lines.filter(n => !!n))});
48
+ });
49
+ }],
50
+ },
51
+ returnResult({reject, resolve, of: 'getList'}, cbk));
52
+ });
53
+ };