balanceofsatoshis 23.1.6 → 23.1.8

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,6 @@
1
1
  # Versions
2
2
 
3
- ## 23.1.6
3
+ ## 23.1.8
4
4
 
5
5
  - Add support for LND 0.21.2
6
6
  - 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');
@@ -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,4 +1,9 @@
1
1
  {
2
+ "allowScripts": {
3
+ "protobufjs@7.6.5": true,
4
+ "ssh2@1.17.0": true,
5
+ "cpu-features@0.0.10": true
6
+ },
2
7
  "author": {
3
8
  "name": "Alex Bosworth",
4
9
  "url": "https://twitter.com/alexbosworth"
@@ -10,7 +15,7 @@
10
15
  "url": "https://github.com/alexbosworth/balanceofsatoshis/issues"
11
16
  },
12
17
  "dependencies": {
13
- "@alexbosworth/blockchain": "4.0.0",
18
+ "@alexbosworth/blockchain": "4.5.1",
14
19
  "@alexbosworth/caporal": "2.0.0",
15
20
  "@alexbosworth/fiat": "2.0.0",
16
21
  "@alexbosworth/formulas": "1.4.0",
@@ -23,20 +28,20 @@
23
28
  "bech32": "2.0.0",
24
29
  "bip66": "2.0.0",
25
30
  "bitcoinjs-lib": "6.1.7",
26
- "bolt01": "3.0.0",
27
- "bolt03": "1.3.2",
28
- "bolt07": "1.9.5",
31
+ "bolt01": "4.0.0",
32
+ "bolt03": "2.0.0",
33
+ "bolt07": "2.0.0",
29
34
  "cbor": "10.0.12",
30
35
  "colorette": "2.0.20",
31
36
  "csv-parse": "7.0.2",
32
- "goldengate": "16.0.5",
37
+ "goldengate": "16.0.6",
33
38
  "grammy": "1.45.1",
34
39
  "import-lazy": "4.0.0",
35
40
  "ini": "7.0.0",
36
- "inquirer": "14.0.2",
41
+ "inquirer": "14.1.0",
37
42
  "ln-accounting": "10.0.5",
38
- "ln-service": "59.2.6",
39
- "ln-sync": "8.0.4",
43
+ "ln-service": "59.3.4",
44
+ "ln-sync": "8.0.5",
40
45
  "ln-telegram": "8.0.5",
41
46
  "moment": "2.30.1",
42
47
  "paid-services": "8.0.6",
@@ -50,8 +55,8 @@
50
55
  },
51
56
  "description": "Lightning balance CLI",
52
57
  "devDependencies": {
53
- "invoices": "6.0.5",
54
- "ln-docker-daemons": "8.0.2"
58
+ "invoices": "6.1.3",
59
+ "ln-docker-daemons": "8.0.4"
55
60
  },
56
61
  "engines": {
57
62
  "node": ">=22"
@@ -69,7 +74,7 @@
69
74
  "name": "balanceofsatoshis",
70
75
  "repository": {
71
76
  "type": "git",
72
- "url": "https://github.com/alexbosworth/balanceofsatoshis.git"
77
+ "url": "git+https://github.com/alexbosworth/balanceofsatoshis.git"
73
78
  },
74
79
  "scripts": {
75
80
  "build-docker": "docker build -t alexbosworth/balanceofsatoshis . && docker save alexbosworth/balanceofsatoshis > balanceofsatoshis.tar && gzip balanceofsatoshis.tar",
@@ -78,5 +83,5 @@
78
83
  "postpublish": "docker buildx build --platform linux/amd64,linux/arm64 -t alexbosworth/balanceofsatoshis -t alexbosworth/balanceofsatoshis:$npm_package_version --push .",
79
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"
80
85
  },
81
- "version": "23.1.6"
86
+ "version": "23.1.8"
82
87
  }
@@ -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
+ };
@@ -2,18 +2,18 @@ const asyncAuto = require('async/auto');
2
2
  const asyncFilter = require('async/filter');
3
3
  const asyncRetry = require('async/retry');
4
4
  const {getChannel} = require('ln-service');
5
+ const {getWalletInfo} = require('ln-service');
5
6
  const {returnResult} = require('asyncjs-util');
6
7
 
7
8
  const appendFailingEdge = require('./append_failing_edge');
9
+ const getAvoidList = require('./get_avoid_list');
8
10
  const rebalance = require('./rebalance');
11
+ const writeAvoidList = require('./write_avoid_list');
9
12
 
10
13
  const channelFromEdge = edge => edge.slice(0, -2);
11
14
  const codeMissingChannel = 404;
12
15
  const {isArray} = Array;
13
16
  const isEdge = n => /^\d*x\d*x\d*x(0|1)*$/.test(n);
14
- const joinWithNewLines = lines => lines.join('\n');
15
- const matchNewLines = /\r?\n/;
16
- const uniq = arr => Array.from(new Set(arr));
17
17
 
18
18
  /** Manage rebalance attempts
19
19
 
@@ -24,6 +24,7 @@ const uniq = arr => Array.from(new Set(arr));
24
24
  fs: {
25
25
  appendFile: <Append to File Function> (path, content, cbk) => {}
26
26
  getFile: <Read File Contents Function> (path, cbk) => {}
27
+ renameFile: <Rename File Function> (from, to, cbk) => {}
27
28
  writeFile: <Write File Contents Function> (path, contents, cbk) => {}
28
29
  }
29
30
  [in_filters]: [<Inbound Filter Formula String>]
@@ -76,31 +77,28 @@ module.exports = (args, cbk) => {
76
77
  return cbk();
77
78
  },
78
79
 
79
- // Get the avoid list and add it to the total avoids
80
+ // Get the avoid directives from the avoid list file
80
81
  getAvoids: ['validate', ({}, cbk) => {
81
82
  // Exit early when there is no ignore list
82
83
  if (!args.avoid_list) {
83
- return cbk(null, {avoid: args.avoid, file: []});
84
+ return cbk(null, {lines: []});
84
85
  }
85
86
 
86
- return args.fs.getFile(args.avoid_list, (err, res) => {
87
- if (!!err) {
88
- return cbk([500, 'UnexpectedErrorFetchingAvoidList', {err}]);
89
- }
90
-
91
- const original = res.toString();
87
+ return getAvoidList({
88
+ fs: {getFile: args.fs.getFile},
89
+ path: args.avoid_list,
90
+ },
91
+ cbk);
92
+ }],
92
93
 
93
- const file = uniq(original
94
- .split(matchNewLines)
95
- .map(line => line.trim())
96
- .filter(line => !!line.length));
94
+ // Get the graph sync status to know if missing channels are reliable
95
+ getGraphSyncStatus: ['validate', ({}, cbk) => {
96
+ // Exit early when there is no ignore list to clean
97
+ if (!args.avoid_list) {
98
+ return cbk();
99
+ }
97
100
 
98
- return cbk(null, {
99
- file,
100
- original,
101
- avoid: file.concat(args.avoid || []),
102
- });
103
- });
101
+ return getWalletInfo({lnd: args.lnd}, cbk);
104
102
  }],
105
103
 
106
104
  // Create failing edge logger for avoid appending
@@ -133,8 +131,17 @@ module.exports = (args, cbk) => {
133
131
  }],
134
132
 
135
133
  // Look at all of the lines in the file and clean them up
136
- getCleanAvoids: ['getAvoids', ({getAvoids}, cbk) => {
137
- return asyncFilter(getAvoids.file, (line, cbk) => {
134
+ getCleanAvoids: [
135
+ 'getAvoids',
136
+ 'getGraphSyncStatus',
137
+ ({getAvoids, getGraphSyncStatus}, cbk) =>
138
+ {
139
+ // Exit early with no info when missing channels may be resurrected
140
+ if (!!getGraphSyncStatus && !getGraphSyncStatus.is_synced_to_graph) {
141
+ return cbk();
142
+ }
143
+
144
+ return asyncFilter(getAvoids.lines, (line, cbk) => {
138
145
  // Exit early when not looking at an edge
139
146
  if (!isEdge(line)) {
140
147
  return cbk(null, true);
@@ -186,7 +193,7 @@ module.exports = (args, cbk) => {
186
193
  cbk => {
187
194
  return rebalance({
188
195
  start,
189
- avoid: getAvoids.avoid,
196
+ avoid: getAvoids.lines.concat(args.avoid || []),
190
197
  fs: args.fs,
191
198
  in_filters: args.in_filters,
192
199
  in_outbound: args.in_outbound,
@@ -208,31 +215,58 @@ module.exports = (args, cbk) => {
208
215
  cbk);
209
216
  }],
210
217
 
211
- // Write a cleaned up avoid list
212
- writeCleanAvoidList: [
218
+ // Determine which lines should be removed from the avoid list
219
+ removals: [
213
220
  'getAvoids',
214
221
  'getCleanAvoids',
215
222
  ({getAvoids, getCleanAvoids}, cbk) =>
216
223
  {
217
- // Exit early when there is no ignore list
218
- if (!args.avoid_list) {
219
- return cbk(null, {avoid: args.avoid, file: []});
224
+ // Exit early with no removals when the clean check had no answer
225
+ if (!getCleanAvoids) {
226
+ return cbk(null, []);
220
227
  }
221
228
 
222
- const cleaned = joinWithNewLines(getCleanAvoids);
229
+ // The clean avoids are the original lines minus missing channels
230
+ const keeping = new Set(getCleanAvoids);
223
231
 
224
- // Exit early when the clean file is identical to the original
225
- if (getAvoids.original === cleaned) {
232
+ // A line that was not kept refers to a channel absent from the graph
233
+ return cbk(null, getAvoids.lines.filter(n => !keeping.has(n)));
234
+ }],
235
+
236
+ // Get the avoid list again to preserve concurrently appended lines
237
+ getCurrentAvoids: ['removals', ({removals}, cbk) => {
238
+ // Exit early when no lines were cleaned out of the avoid list
239
+ if (!removals.length) {
226
240
  return cbk();
227
241
  }
228
242
 
229
- return args.fs.writeFile(args.avoid_list, cleaned, err => {
230
- if (!!err) {
231
- return cbk([503, 'UnexpectedErrorWritingCleanedAvoidList', {err}]);
232
- }
243
+ return getAvoidList({
244
+ fs: {getFile: args.fs.getFile},
245
+ path: args.avoid_list,
246
+ },
247
+ cbk);
248
+ }],
233
249
 
250
+ // Write a cleaned up avoid list
251
+ writeCleanAvoidList: [
252
+ 'getCurrentAvoids',
253
+ 'removals',
254
+ ({getCurrentAvoids, removals}, cbk) =>
255
+ {
256
+ // Exit early when there is no cleaned avoid list to write out
257
+ if (!getCurrentAvoids) {
234
258
  return cbk();
235
- });
259
+ }
260
+
261
+ const removing = new Set(removals);
262
+
263
+ // Atomically write the avoid list without the cleaned out lines
264
+ return writeAvoidList({
265
+ fs: {renameFile: args.fs.renameFile, writeFile: args.fs.writeFile},
266
+ lines: getCurrentAvoids.lines.filter(n => !removing.has(n)),
267
+ path: args.avoid_list,
268
+ },
269
+ cbk);
236
270
  }],
237
271
  },
238
272
  returnResult({reject, resolve, of: 'rebalance'}, cbk));
@@ -0,0 +1,68 @@
1
+ const asyncAuto = require('async/auto');
2
+ const {returnResult} = require('asyncjs-util');
3
+
4
+ const {isArray} = Array;
5
+ const joinWithNewLines = lines => lines.join('\n');
6
+ const tempFile = path => `${path}.${process.pid}`;
7
+
8
+ /** Atomically write the lines of an avoid list file
9
+
10
+ The list is written to a temporary file that is swapped into place so that
11
+ concurrent readers of the list never see a partially written file.
12
+
13
+ {
14
+ fs: {
15
+ renameFile: <Rename File Function> (from, to, cbk) => {}
16
+ writeFile: <Write File Contents Function> (path, contents, cbk) => {}
17
+ }
18
+ lines: [<Avoid Directive Line String>]
19
+ path: <Path To Avoid List File String>
20
+ }
21
+
22
+ @returns via cbk or Promise
23
+ */
24
+ module.exports = ({fs, lines, path}, cbk) => {
25
+ return new Promise((resolve, reject) => {
26
+ return asyncAuto({
27
+ // Check arguments
28
+ validate: cbk => {
29
+ if (!fs) {
30
+ return cbk([400, 'ExpectedFilesystemMethodsToWriteAvoidList']);
31
+ }
32
+
33
+ if (!isArray(lines)) {
34
+ return cbk([400, 'ExpectedAvoidLinesToWriteAvoidList']);
35
+ }
36
+
37
+ if (!path) {
38
+ return cbk([400, 'ExpectedPathToAvoidListFileToWriteAvoidList']);
39
+ }
40
+
41
+ return cbk();
42
+ },
43
+
44
+ // Write the list to a temporary file next to the avoid list
45
+ write: ['validate', ({}, cbk) => {
46
+ return fs.writeFile(tempFile(path), joinWithNewLines(lines), err => {
47
+ if (!!err) {
48
+ return cbk([503, 'UnexpectedErrorWritingAvoidList', {err}]);
49
+ }
50
+
51
+ return cbk();
52
+ });
53
+ }],
54
+
55
+ // Atomically swap the temporary file in place of the avoid list
56
+ swap: ['write', ({}, cbk) => {
57
+ return fs.renameFile(tempFile(path), path, err => {
58
+ if (!!err) {
59
+ return cbk([503, 'UnexpectedErrorReplacingAvoidList', {err}]);
60
+ }
61
+
62
+ return cbk();
63
+ });
64
+ }],
65
+ },
66
+ returnResult({reject, resolve}, cbk));
67
+ });
68
+ };
@@ -0,0 +1,62 @@
1
+ const {deepEqual} = require('node:assert').strict;
2
+ const {rejects} = require('node:assert').strict;
3
+ const test = require('node:test');
4
+
5
+ const getAvoidList = require('./../../swaps/get_avoid_list.js');
6
+
7
+ const makeFs = ({content, err} = {}) => {
8
+ return {getFile: (path, cbk) => cbk(err, Buffer.from(content || ''))};
9
+ };
10
+
11
+ const makeArgs = override => {
12
+ const args = {
13
+ fs: makeFs({content: '\n1x1x1x0\n b \n\na\r\n1x1x1x0\nc'}),
14
+ path: '/tmp/avoid',
15
+ };
16
+
17
+ Object.keys(override).forEach(key => args[key] = override[key]);
18
+
19
+ return args;
20
+ };
21
+
22
+ const tests = [
23
+ {
24
+ args: makeArgs({fs: undefined}),
25
+ description: 'Filesystem methods are required',
26
+ error: [400, 'ExpectedFilesystemMethodsToGetAvoidList'],
27
+ },
28
+ {
29
+ args: makeArgs({path: undefined}),
30
+ description: 'An avoid list path is required',
31
+ error: [400, 'ExpectedPathToAvoidListFileToGetAvoidList'],
32
+ },
33
+ {
34
+ args: makeArgs({fs: makeFs({err: new Error('Failed to read')})}),
35
+ description: 'A read error is returned',
36
+ error: [503, 'UnexpectedErrorGettingAvoidListFile'],
37
+ },
38
+ {
39
+ args: makeArgs({fs: makeFs({})}),
40
+ description: 'An empty file has no lines',
41
+ expected: {lines: []},
42
+ },
43
+ {
44
+ args: makeArgs({}),
45
+ description: 'Lines are trimmed, deduplicated, and blanks are removed',
46
+ expected: {lines: ['1x1x1x0', 'b', 'a', 'c']},
47
+ },
48
+ ];
49
+
50
+ tests.forEach(({args, description, error, expected}) => {
51
+ return test(description, async () => {
52
+ if (!!error) {
53
+ await rejects(getAvoidList(args), error, 'Got expected error');
54
+ } else {
55
+ const result = await getAvoidList(args);
56
+
57
+ deepEqual(result, expected, 'Got expected result');
58
+ }
59
+
60
+ return;
61
+ });
62
+ });
@@ -0,0 +1,83 @@
1
+ const {deepEqual} = require('node:assert').strict;
2
+ const {rejects} = require('node:assert').strict;
3
+ const test = require('node:test');
4
+
5
+ const writeAvoidList = require('./../../swaps/write_avoid_list.js');
6
+
7
+ const temp = `/tmp/avoid.${process.pid}`;
8
+
9
+ const makeFs = ({renameErr, writeErr} = {}) => {
10
+ const fs = {
11
+ calls: [],
12
+ renameFile: (from, to, cbk) => {
13
+ fs.calls.push({from, to});
14
+
15
+ return cbk(renameErr);
16
+ },
17
+ writeFile: (path, contents, cbk) => {
18
+ fs.calls.push({contents, path});
19
+
20
+ return cbk(writeErr);
21
+ },
22
+ };
23
+
24
+ return fs;
25
+ };
26
+
27
+ const makeArgs = override => {
28
+ const args = {fs: makeFs({}), lines: ['a', 'b'], path: '/tmp/avoid'};
29
+
30
+ Object.keys(override).forEach(key => args[key] = override[key]);
31
+
32
+ return args;
33
+ };
34
+
35
+ const tests = [
36
+ {
37
+ args: makeArgs({fs: undefined}),
38
+ description: 'Filesystem methods are required',
39
+ error: [400, 'ExpectedFilesystemMethodsToWriteAvoidList'],
40
+ },
41
+ {
42
+ args: makeArgs({lines: undefined}),
43
+ description: 'Avoid lines are required',
44
+ error: [400, 'ExpectedAvoidLinesToWriteAvoidList'],
45
+ },
46
+ {
47
+ args: makeArgs({path: undefined}),
48
+ description: 'An avoid list path is required',
49
+ error: [400, 'ExpectedPathToAvoidListFileToWriteAvoidList'],
50
+ },
51
+ {
52
+ args: makeArgs({fs: makeFs({writeErr: new Error('WriteFailed')})}),
53
+ description: 'A write error is returned',
54
+ error: [503, 'UnexpectedErrorWritingAvoidList'],
55
+ },
56
+ {
57
+ args: makeArgs({fs: makeFs({renameErr: new Error('RenameFailed')})}),
58
+ description: 'A rename error is returned',
59
+ error: [503, 'UnexpectedErrorReplacingAvoidList'],
60
+ },
61
+ {
62
+ args: makeArgs({}),
63
+ calls: [
64
+ {contents: 'a\nb', path: temp},
65
+ {from: temp, to: '/tmp/avoid'},
66
+ ],
67
+ description: 'The list is written to a temp file and swapped in place',
68
+ },
69
+ ];
70
+
71
+ tests.forEach(({args, calls, description, error}) => {
72
+ return test(description, async () => {
73
+ if (!!error) {
74
+ await rejects(writeAvoidList(args), error, 'Got expected error');
75
+ } else {
76
+ await writeAvoidList(args);
77
+
78
+ deepEqual(args.fs.calls, calls, 'Got expected filesystem calls');
79
+ }
80
+
81
+ return;
82
+ });
83
+ });