balanceofsatoshis 25.4.0 → 25.6.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.
Files changed (59) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +4 -1
  3. package/bos +49 -0
  4. package/chain/fund_transaction.js +10 -9
  5. package/chain/get_block_transactions.js +6 -5
  6. package/chain/recover_p2pk.js +5 -4
  7. package/chain/split_utxos.js +5 -4
  8. package/commands/fetch_request.js +32 -24
  9. package/display/describe_routing_failure.js +6 -2
  10. package/display/get_icons.js +33 -22
  11. package/encryption/cbor_item_head.js +45 -0
  12. package/encryption/decode_cbor.js +35 -0
  13. package/encryption/decode_cbor_item.js +169 -0
  14. package/encryption/decrypt_with_node.js +21 -18
  15. package/encryption/decrypt_with_secret.js +5 -4
  16. package/encryption/encode_as_cbor.js +5 -12
  17. package/encryption/encode_cbor_item.js +90 -0
  18. package/encryption/index.js +2 -0
  19. package/lnd/get_socket.js +11 -10
  20. package/lnurl/auth.js +6 -5
  21. package/lnurl/get_pay_request.js +8 -3
  22. package/lnurl/get_pay_terms.js +12 -3
  23. package/lnurl/parse_url.js +18 -4
  24. package/network/constants.json +1 -0
  25. package/network/execute_probe.js +14 -11
  26. package/network/get_out_channels.js +128 -0
  27. package/network/get_out_queries.js +102 -0
  28. package/network/get_peers.js +9 -4
  29. package/network/get_stream_invoice.js +95 -0
  30. package/network/get_stream_route.js +153 -0
  31. package/network/index.js +2 -0
  32. package/network/line_writer.js +29 -0
  33. package/network/pay_stream_route.js +103 -0
  34. package/network/probe.js +2 -0
  35. package/network/probe_destination.js +97 -68
  36. package/network/push_payment.js +42 -17
  37. package/network/send_gift.js +10 -9
  38. package/network/stream_payment.js +591 -0
  39. package/network/transfer_funds.js +6 -6
  40. package/nodes/register_node.js +34 -30
  41. package/offchain/create_invoice.js +21 -16
  42. package/offchain/get_invoice_amount.js +5 -4
  43. package/offchain/sign_payment_request.js +10 -9
  44. package/package.json +5 -6
  45. package/peers/detect_open_rule_violation.js +6 -5
  46. package/routing/get_chain_fees_chart.js +3 -2
  47. package/routing/get_fees_paid.js +30 -12
  48. package/services/initiate_balanced_channel.js +13 -7
  49. package/swaps/append_failing_edge.js +6 -5
  50. package/swaps/decode_swap_api_key.js +29 -25
  51. package/swaps/get_paid_service.js +38 -17
  52. package/swaps/get_routes_for_funding.js +52 -11
  53. package/swaps/get_swap_service.js +5 -4
  54. package/swaps/rebalance.js +78 -63
  55. package/swaps/swap_in.js +36 -26
  56. package/tags/get_tags.js +26 -15
  57. package/telegram/connect_to_telegram.js +17 -12
  58. package/telegram/get_socks_proxy.js +12 -13
  59. package/telegram/get_telegram_bot.js +2 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Versions
2
2
 
3
+ ## 25.6.0
4
+
5
+ - `stream`: Add command to stream a payment to a lnurl or lightning address
6
+
7
+ ## 25.5.0
8
+
9
+ - `probe`: Add `--ignore-expiry` to probe expired payment requests
10
+
3
11
  ## 25.4.0
4
12
 
5
13
  - `accounting`: After 2021-10-19 derive USD prices from chain payments data
package/README.md CHANGED
@@ -12,7 +12,7 @@ Supported LND versions:
12
12
 
13
13
  ## Install
14
14
 
15
- - Requires an [installation of Node v20+][nodejs-install-guide]
15
+ - Requires an [installation of Node.js v22+][nodejs-install-guide]
16
16
 
17
17
  ```shell
18
18
  npm install -g balanceofsatoshis
@@ -168,6 +168,9 @@ bos send
168
168
  # Run a server of paid services
169
169
  bos server
170
170
 
171
+ # Stream a payment to a lnurl or lightning address in a series of payments
172
+ bos stream "amount" --lnurl "lnurl/lightning_address"
173
+
171
174
  # Connect up to a Telegram bot
172
175
  bos telegram
173
176
 
package/bos CHANGED
@@ -1657,6 +1657,7 @@ prog
1657
1657
  .option('--avoid <avoid>', 'Avoid forwarding via node/chan/tag', REPEATABLE)
1658
1658
  .option('--avoid-high-fee-routes', 'Avoid evaluating too-high fee routes')
1659
1659
  .option('--find-max', 'Find the maximum routeable amount on success route')
1660
+ .option('--ignore-expiry', 'Probe even when the request has expired')
1660
1661
  .option('--in <public_key>', 'Route through specific peer of destination')
1661
1662
  .option('--max-fee <max_fee>', 'Max expected fee for route', INT, 1337)
1662
1663
  .option('--max-paths <max>', 'Maximum paths to use for find-max', INT, 1)
@@ -1679,6 +1680,7 @@ prog
1679
1680
  find_max: !!options.findMax ? 16777215 : undefined,
1680
1681
  fs: {getFile: readFile},
1681
1682
  in_through: inThrough.public_key,
1683
+ is_ignoring_expiry: !!options.ignoreExpiry,
1682
1684
  is_strict_max_fee: !!options.avoidHighFeeRoutes,
1683
1685
  max_fee: options.maxFee,
1684
1686
  max_paths: options.maxPaths,
@@ -1952,6 +1954,53 @@ prog
1952
1954
  });
1953
1955
  })
1954
1956
 
1957
+ // Stream a payment in a series of payments
1958
+ .command('stream', 'Stream a payment in a series of payments')
1959
+ .help('Each payment is sized to the most a single route can carry')
1960
+ .help('--lnurl is required, it can stream above the max the service takes')
1961
+ .help('--min-payment defaults to 100*k, a smaller remainder is not sent')
1962
+ .help('--out can take a tag to make the first hop through any tagged peer')
1963
+ .help('--out-filter is checked before every payment, skipping failing peers')
1964
+ .help('--out-filter vars: CAPACITY/HEIGHTS/INBOUND_LIQUIDITY')
1965
+ .help('--out-filter vars: OUTBOUND_LIQUIDITY/PENDING_PAYMENTS')
1966
+ .help('--out-filter vars: INBOUND_BASE_FEE/INBOUND_FEE_RATE')
1967
+ .help('Amounts can take m/k variables: 5*m for 5 million, 250*k = 0.0025')
1968
+ .argument('<amount>', 'Total amount to stream')
1969
+ .option('--avoid <avoid>', 'Avoid forwarding via node/chan/tag', REPEATABLE)
1970
+ .option('--lnurl <url>', 'Lnurl/lightning address', STRING, undefined, true)
1971
+ .option('--max-fee <max_fee>', 'Maximum total fee to pay', STRING, '1337')
1972
+ .option('--max-fee-rate <ppm>', 'Max fee rate for all payments', INT, 1000)
1973
+ .option('--max-payment <amount>', 'Largest single payment to make', STRING)
1974
+ .option('--min-payment <amount>', 'Smallest single payment to make', STRING)
1975
+ .option('--no-color', 'Mute all colors')
1976
+ .option('--node <node_name>', 'Node to stream from')
1977
+ .option('--out <peer_or_tag>', 'Make first hop through peer/tag', REPEATABLE)
1978
+ .option('--out-filter <out_filter>', 'Filter outbound peer/tag', REPEATABLE)
1979
+ .action((args, options, logger) => {
1980
+ return new Promise(async (resolve, reject) => {
1981
+ try {
1982
+ return network.streamPayment({
1983
+ logger,
1984
+ amount: args.amount,
1985
+ avoid: flatten([options.avoid].filter(n => !!n)),
1986
+ fs: {getFile: readFile},
1987
+ lnd: (await lndForNode(logger, options.node)).lnd,
1988
+ lnurl: options.lnurl,
1989
+ max_fee: options.maxFee,
1990
+ max_fee_rate: options.maxFeeRate,
1991
+ max_payment: options.maxPayment,
1992
+ min_payment: options.minPayment,
1993
+ out: flatten([options.out].filter(n => !!n)),
1994
+ out_filters: flatten([options.outFilter].filter(n => !!n)),
1995
+ request: commands.simpleRequest,
1996
+ },
1997
+ responses.returnObject({logger, reject, resolve}));
1998
+ } catch (err) {
1999
+ return logger.error({err}) && reject();
2000
+ }
2001
+ });
2002
+ })
2003
+
1955
2004
  // Get a swap API key or inspect an existing one
1956
2005
  .command('swap-api-key', 'Purchase a swap API key or inspect a swap API key')
1957
2006
  .visible(false)
@@ -1,5 +1,6 @@
1
1
  const asyncAuto = require('async/auto');
2
2
  const asyncEach = require('async/each');
3
+ const asyncify = require('async/asyncify');
3
4
  const {broadcastTransaction} = require('ln-sync');
4
5
  const {componentsOfTransaction} = require('@alexbosworth/blockchain');
5
6
  const {formatTokens} = require('ln-sync');
@@ -112,10 +113,10 @@ module.exports = (args, cbk) => {
112
113
  getNetwork: ['validate', ({}, cbk) => getNetwork({lnd: args.lnd}, cbk)],
113
114
 
114
115
  // Derive a list of outputs to guide input selection
115
- outputs: ['validate', ({}, cbk) => {
116
+ outputs: ['validate', asyncify(() => {
116
117
  // Exit early when the amount is open ended and thus depends on inputs
117
118
  if (hasMaxAmount(args.amounts)) {
118
- return cbk();
119
+ return;
119
120
  }
120
121
 
121
122
  try {
@@ -125,11 +126,11 @@ module.exports = (args, cbk) => {
125
126
  return {address, tokens};
126
127
  });
127
128
 
128
- return cbk(null, outputs);
129
+ return outputs;
129
130
  } catch (err) {
130
- return cbk([400, err.message]);
131
+ throw [400, err.message];
131
132
  }
132
- }],
133
+ })],
133
134
 
134
135
  // Get UTXOs to use for input selection and final fee rate calculation
135
136
  getUtxos: ['validate', ({}, cbk) => getUtxos({lnd: args.lnd}, cbk)],
@@ -238,7 +239,7 @@ module.exports = (args, cbk) => {
238
239
  }],
239
240
 
240
241
  // Parse amounts and put together the final set of outputs
241
- finalOutputs: ['getMax', ({getMax}, cbk) => {
242
+ finalOutputs: ['getMax', asyncify(({getMax}) => {
242
243
  try {
243
244
  const outputs = args.addresses.map((address, i) => {
244
245
  const amount = args.amounts[i];
@@ -252,11 +253,11 @@ module.exports = (args, cbk) => {
252
253
  return {address, tokens: parseAmount({amount, variables}).tokens};
253
254
  });
254
255
 
255
- return cbk(null, outputs);
256
+ return outputs;
256
257
  } catch (err) {
257
- return cbk([400, err.message]);
258
+ throw [400, err.message];
258
259
  }
259
- }],
260
+ })],
260
261
 
261
262
  // Create a funded PSBT
262
263
  fund: [
@@ -1,4 +1,5 @@
1
1
  const asyncAuto = require('async/auto');
2
+ const asyncify = require('async/asyncify');
2
3
  const {getBlock} = require('ln-service');
3
4
  const {queryTransactions} = require('@alexbosworth/blockchain');
4
5
  const {returnResult} = require('asyncjs-util');
@@ -77,10 +78,10 @@ module.exports = (args, cbk) => {
77
78
  }],
78
79
 
79
80
  // Find the transactions in the block
80
- transactions: ['getBlock', ({getBlock}, cbk) => {
81
+ transactions: ['getBlock', asyncify(({getBlock}) => {
81
82
  // Exit early when there is no block to look through
82
83
  if (!getBlock.block) {
83
- return cbk(null, {transactions: []});
84
+ return {transactions: []};
84
85
  }
85
86
 
86
87
  try {
@@ -89,11 +90,11 @@ module.exports = (args, cbk) => {
89
90
  ids: args.ids,
90
91
  });
91
92
 
92
- return cbk(null, {transactions});
93
+ return {transactions};
93
94
  } catch (err) {
94
- return cbk([503, 'UnexpectedErrorParsingBlockTransactions', {err}]);
95
+ throw [503, 'UnexpectedErrorParsingBlockTransactions', {err}];
95
96
  }
96
- }],
97
+ })],
97
98
  },
98
99
  returnResult({reject, resolve, of: 'transactions'}, cbk));
99
100
  });
@@ -1,6 +1,7 @@
1
1
  const {createHash} = require('crypto');
2
2
 
3
3
  const asyncAuto = require('async/auto');
4
+ const asyncify = require('async/asyncify');
4
5
  const {broadcastChainTransaction} = require('ln-service');
5
6
  const {componentsOfTransaction} = require('@alexbosworth/blockchain');
6
7
  const {createChainAddress} = require('ln-service');
@@ -135,7 +136,7 @@ module.exports = ({id, lnd, request, vout}, cbk) => {
135
136
  recoveryScript: [
136
137
  'createAddress',
137
138
  'getNetwork',
138
- ({createAddress, getNetwork}, cbk) =>
139
+ asyncify(({createAddress, getNetwork}) =>
139
140
  {
140
141
  try {
141
142
  const {script} = outputScriptForAddress({
@@ -143,11 +144,11 @@ module.exports = ({id, lnd, request, vout}, cbk) => {
143
144
  network: getNetwork.network,
144
145
  });
145
146
 
146
- return cbk(null, {script});
147
+ return {script};
147
148
  } catch (err) {
148
- return cbk([503, 'FailedToParseRecoveryAddress', {err}]);
149
+ throw [503, 'FailedToParseRecoveryAddress', {err}];
149
150
  }
150
- }],
151
+ })],
151
152
 
152
153
  // Derive the sweep output, less the amount needed to pay for chain fees
153
154
  sweep: [
@@ -1,5 +1,6 @@
1
1
  const asyncAuto = require('async/auto');
2
2
  const asyncEach = require('async/each');
3
+ const asyncify = require('async/asyncify');
3
4
  const asyncMapSeries = require('async/mapSeries');
4
5
  const asyncReflect = require('async/reflect');
5
6
  const {broadcastChainTransaction} = require('ln-service');
@@ -85,7 +86,7 @@ module.exports = (args, cbk) => {
85
86
  getUtxos: ['validate', ({}, cbk) => getUtxos({lnd: args.lnd}, cbk)],
86
87
 
87
88
  // Select the inputs and the count of outputs that reach the count
88
- plan: ['feeRate', 'getUtxos', ({feeRate, getUtxos}, cbk) => {
89
+ plan: ['feeRate', 'getUtxos', asyncify(({feeRate, getUtxos}) => {
89
90
  try {
90
91
  const plan = utxoSplitPlan({
91
92
  count: args.count,
@@ -95,11 +96,11 @@ module.exports = (args, cbk) => {
95
96
  utxos: getUtxos.utxos,
96
97
  });
97
98
 
98
- return cbk(null, plan);
99
+ return plan;
99
100
  } catch (err) {
100
- return cbk([400, err.message]);
101
+ throw [400, err.message];
101
102
  }
102
- }],
103
+ })],
103
104
 
104
105
  // Generate addresses
105
106
  getAddresses: ['plan', ({plan}, cbk) => {
@@ -10,14 +10,12 @@ const timeoutSignals = new WeakMap();
10
10
  }
11
11
 
12
12
  @returns
13
- {
14
- request: <Request Function>
15
- }
13
+ <Request Function> (args, cbk) => {}
16
14
  */
17
15
  module.exports = ({fetch}, cbk) => {
18
16
  return (options, cbk) => {
19
17
  (async () => {
20
- const controller = new AbortController();
18
+ const controller = new AbortController();
21
19
 
22
20
  // Setup the timeout controller
23
21
  if (!!options.timeout) {
@@ -28,30 +26,40 @@ module.exports = ({fetch}, cbk) => {
28
26
 
29
27
  const url = options.url + encodeQs(options.qs);
30
28
 
31
- try {
32
- // Make the request
33
- const response = await fetch(url, {
34
- body: options.body || undefined,
35
- method: options.method || undefined,
36
- signal: controller.signal,
37
- });
29
+ // Only the request is guarded, so the callback is called once
30
+ const fetched = await (async () => {
31
+ try {
32
+ // Make the request
33
+ const response = await fetch(url, {
34
+ body: options.body || undefined,
35
+ method: options.method || undefined,
36
+ signal: controller.signal,
37
+ });
38
38
 
39
- const res = {statusCode: response.status};
39
+ const res = {statusCode: response.status};
40
40
 
41
- if (!!options.json) {
42
- return cbk(null, res, await response.json());
43
- } else {
44
- return cbk(null, res, await response.text());
45
- }
46
- } catch (err) {
47
- // Return request errors back
48
- return cbk(err);
49
- } finally {
50
- // Reset the timeout since the request is over
51
- if (!!options.timeout) {
52
- clearTimeout(timeoutSignals.get(controller.signal));
41
+ // Exit early when the response is JSON
42
+ if (!!options.json) {
43
+ return {res, body: await response.json()};
44
+ }
45
+
46
+ return {res, body: await response.text()};
47
+ } catch (err) {
48
+ return {err};
49
+ } finally {
50
+ // Reset the timeout since the request is over
51
+ if (!!options.timeout) {
52
+ clearTimeout(timeoutSignals.get(controller.signal));
53
+ }
53
54
  }
55
+ })();
56
+
57
+ // Exit early when the request failed
58
+ if (!!fetched.err) {
59
+ return cbk(fetched.err);
54
60
  }
61
+
62
+ return cbk(null, fetched.res, fetched.body);
55
63
  })();
56
64
  };
57
65
  };
@@ -108,6 +108,7 @@ module.exports = ({index, lnd, reason, route, tagged}, cbk) => {
108
108
 
109
109
  const {display} = chartAliasForPeer({
110
110
  alias: getTo.alias,
111
+ icons: !!tag ? tag.icons : undefined,
111
112
  public_key: getTo.id,
112
113
  });
113
114
 
@@ -120,11 +121,14 @@ module.exports = ({index, lnd, reason, route, tagged}, cbk) => {
120
121
  'toName',
121
122
  ({fromName, toName}, cbk) =>
122
123
  {
123
- const at = `on ${route.hops[index].channel}`;
124
+ const hop = route.hops[index];
125
+
126
+ // A failure at the destination is not on a channel out of it
127
+ const at = !hop ? '' : ` on ${hop.channel}`;
124
128
  const from = !fromName ? '' : `from ${fromName}`;
125
129
  const to = !toName ? '' : ` forwarding to ${toName}`;
126
130
 
127
- return cbk(null, {description: `${reason} ${from} ${at}${to}`});
131
+ return cbk(null, {description: `${reason} ${from}${at}${to}`});
128
132
  }],
129
133
  },
130
134
  returnResult({reject, resolve, of: 'description'}, cbk));
@@ -1,4 +1,5 @@
1
1
  const asyncAuto = require('async/auto');
2
+ const asyncify = require('async/asyncify');
2
3
  const {returnResult} = require('asyncjs-util');
3
4
 
4
5
  const {homePath} = require('../storage');
@@ -38,38 +39,48 @@ module.exports = ({fs}, cbk) => {
38
39
  return cbk();
39
40
  },
40
41
 
41
- // Get icons from the tags file
42
- getIcons: ['validate', ({}, cbk) => {
42
+ // Get the tags file
43
+ getFile: ['validate', ({}, cbk) => {
43
44
  return fs.getFile(tagFilePath(), (err, res) => {
44
45
  // Exit early when there is no tag file
45
46
  if (!!err || !res) {
46
- return cbk(null, {nodes: []});
47
+ return cbk();
47
48
  }
48
49
 
49
- try {
50
- const file = parse(res.toString());
50
+ return cbk(null, res.toString());
51
+ });
52
+ }],
53
+
54
+ // Get icons from the tags file
55
+ getIcons: ['getFile', asyncify(({getFile}) => {
56
+ // Exit early when there is no tag file
57
+ if (!getFile) {
58
+ return {nodes: []};
59
+ }
51
60
 
52
- const keys = uniq(flatten(file.tags.map(n => n.nodes)));
61
+ try {
62
+ const file = parse(getFile);
53
63
 
54
- const nodes = keys.map(key => {
55
- // Only tags this node is included in
56
- const meta = file.tags.filter(tag => {
57
- return isArray(tag.nodes) && tag.nodes.includes(key);
58
- });
64
+ const keys = uniq(flatten(file.tags.map(n => n.nodes)));
59
65
 
60
- return {
61
- aliases: uniq(meta.map(n => n.alias)),
62
- icons: uniq(meta.map(n => n.icon)),
63
- public_key: key,
64
- };
66
+ const nodes = keys.map(key => {
67
+ // Only tags this node is included in
68
+ const meta = file.tags.filter(tag => {
69
+ return isArray(tag.nodes) && tag.nodes.includes(key);
65
70
  });
66
71
 
67
- return cbk(null, {nodes});
68
- } catch (err) {
69
- return cbk(null, {nodes: []});
70
- }
71
- });
72
- }],
72
+ return {
73
+ aliases: uniq(meta.map(n => n.alias)),
74
+ icons: uniq(meta.map(n => n.icon)),
75
+ public_key: key,
76
+ };
77
+ });
78
+
79
+ return {nodes};
80
+ } catch (err) {
81
+ return {nodes: []};
82
+ }
83
+ })],
73
84
  },
74
85
  returnResult({reject, resolve, of: 'getIcons'}, cbk));
75
86
  });
@@ -0,0 +1,45 @@
1
+ const additionalInfo = {1: 24, 2: 25, 4: 26, 8: 27};
2
+ const bitsPerByte = 8;
3
+ const largestSize = 8;
4
+ const majorTypeMultiplier = 32;
5
+ const maxInitialNumber = 23;
6
+ const sizes = [1, 2, 4];
7
+
8
+ /** Encode the head of a CBOR data item, its major type and its number
9
+
10
+ {
11
+ major: <Major Type Number>
12
+ number: <Length or Value Number>
13
+ }
14
+
15
+ @returns
16
+ {
17
+ head: <Data Item Head Buffer Object>
18
+ }
19
+ */
20
+ module.exports = ({major, number}) => {
21
+ const initial = major * majorTypeMultiplier;
22
+
23
+ // Exit early when the number fits into the initial byte
24
+ if (number <= maxInitialNumber) {
25
+ return {head: Buffer.from([initial + number])};
26
+ }
27
+
28
+ // The number follows the initial byte, in as few bytes as it fits into
29
+ const size = sizes.find(n => number < 2 ** (n * bitsPerByte)) || largestSize;
30
+
31
+ const head = Buffer.alloc(1 + size);
32
+
33
+ head[0] = initial + additionalInfo[size];
34
+
35
+ // Exit early when the number needs all eight bytes
36
+ if (size === largestSize) {
37
+ head.writeBigUInt64BE(BigInt(number), 1);
38
+
39
+ return {head};
40
+ }
41
+
42
+ head.writeUIntBE(number, 1, size);
43
+
44
+ return {head};
45
+ };
@@ -0,0 +1,35 @@
1
+ const decodeCborItem = require('./decode_cbor_item');
2
+
3
+ const hexAsBuffer = hex => Buffer.from(hex, 'hex');
4
+ const isString = n => typeof n === 'string';
5
+
6
+ /** Decode CBOR encoded data
7
+
8
+ {
9
+ cbor: <CBOR Encoded Hex String>
10
+ }
11
+
12
+ @throws
13
+ <Error>
14
+
15
+ @returns
16
+ {
17
+ value: <Decoded Value Object>
18
+ }
19
+ */
20
+ module.exports = ({cbor}) => {
21
+ if (!isString(cbor)) {
22
+ throw new Error('ExpectedHexEncodedCborToDecode');
23
+ }
24
+
25
+ const data = hexAsBuffer(cbor);
26
+
27
+ const {end, value} = decodeCborItem({data, start: 0});
28
+
29
+ // The data has to be a single item, with nothing left over
30
+ if (end !== data.length) {
31
+ throw new Error('UnexpectedDataAfterCborItem');
32
+ }
33
+
34
+ return {value};
35
+ };