balanceofsatoshis 24.0.1 → 24.2.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,18 @@
1
1
  # Versions
2
2
 
3
+ ## 24.2.0
4
+
5
+ - Add support for LND 0.21.4 and LND 0.20.5
6
+ - `pay`, `probe`: Add support for tags in `--out`, `--out-filter`
7
+
8
+ ## 24.1.0
9
+
10
+ - `fees`: Add `--repeat-interval-ms` to show or set the fees again each time an
11
+ interval elapses, such as `5*min` to keep formula fee rates up to date
12
+ - `rebalance`: Add `--repeat-interval-ms` to run the rebalance again each time
13
+ an interval elapses, such as `30*min`, `1*h * (0.5 + RAND())` for a random
14
+ interval, or `1*min * 2^FAILURES_COUNT` to back off on failures in a row
15
+
3
16
  ## 24.0.1
4
17
 
5
18
  ### Breaking Changes
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.3-beta
11
- - v0.20.0-beta to v0.20.4-beta
10
+ - v0.21.0-beta to v0.21.4-beta
11
+ - v0.20.0-beta to v0.20.5-beta
12
12
 
13
13
  ## Install
14
14
 
@@ -327,18 +327,17 @@ expr $(bos balance --node=savedNode1) + $(bos balance --node=savedNode2)
327
327
 
328
328
  ### Auto Adjust Fees
329
329
 
330
- ```
331
- # Cron every 5 minutes adjust fees
332
- */5 * * * * /bin/timeout -s 2 30 /home/ubuntu/update-fees.sh
333
- ```
334
-
335
- update-fees.sh:
330
+ Repeat the `fees` command on an interval to keep a fee rate formula up to date
336
331
 
332
+ ```shell
333
+ # Every 5 minutes, raise the outbound fees to a public key when inbound increases
334
+ bos fees --to PUBLIC_KEY --set-fee-rate="IF(INBOUND>10000000,1000,500)" --repeat-interval-ms="5*min"
337
335
  ```
338
- #!/bin/bash
339
- # Raise the outbound fees to a public key when inbound increases
340
- /home/ubuntu/.npm-global/bin/bos fees --to PUBLIC_KEY --set-fee-rate="IF(INBOUND>10000000,1000,500)"
341
- ```
336
+
337
+ The interval is a formula in milliseconds that can use `s`, `min`, `h`, `d` and
338
+ `w` units, so `"5*min * (0.5 + RAND())"` repeats at a random interval between
339
+ 2.5 and 7.5 minutes. See "Persist Long-Running Commands" below to keep the
340
+ command running.
342
341
 
343
342
  ### Auto Balance Liquidity Between Two Nodes
344
343
 
@@ -431,13 +430,11 @@ Docker example:
431
430
  docker run -d --restart always -v $HOME/.bos:/home/node/.bos alexbosworth/balanceofsatoshis telegram --connect CONNECT_CODE
432
431
  ```
433
432
 
434
- You can also create a shell-script.sh to run a command repeatedly, with a delay
433
+ Commands like `fees` and `rebalance` can also repeat themselves, with a delay
435
434
 
436
- ```bash
437
- while true;
438
- do bos rebalance;
439
- sleep 2000;
440
- done
435
+ ```shell
436
+ # Rebalance again 30 minutes after each attempt, or 1 hour when it failed
437
+ bos rebalance --repeat-interval-ms="IF(FAILURES_COUNT > 0, 1*h, 30*min)"
441
438
  ```
442
439
 
443
440
  ## Docker
package/bos CHANGED
@@ -14,7 +14,6 @@ const {writeFile} = require('fs');
14
14
  const importLazy = require('import-lazy')(require);
15
15
 
16
16
  const accounting = importLazy('ln-accounting');
17
- const fetch = importLazy('@alexbosworth/node-fetch');
18
17
  const fiat = importLazy('@alexbosworth/fiat');
19
18
  const lnService = importLazy('ln-service');
20
19
  const lnSync = importLazy('ln-sync');
@@ -731,7 +730,9 @@ prog
731
730
  .help('You can use INBOUND_FEE_RATE to mirror an inbound fee')
732
731
  .help('You can use FEE_RATE_OF_<PUBKEY> to reference other node rates')
733
732
  .help('--set-inbound-discount-rate is total discount on traffic from peer')
733
+ .help('--repeat-interval-ms vars: s, min, h, d, w: 1*h repeats hourly')
734
734
  .option('--node <node_name>', 'Saved node (not peer to set fees on)')
735
+ .option('--repeat-interval-ms <interval>', 'Repeat fees after interval')
735
736
  .option('--set-cltv-delta <count>', 'Set the number of blocks for CLTV', INT)
736
737
  .option('--set-fee-rate <rate>', 'Fee in parts per million or use a formula')
737
738
  .option('--set-inbound-rate-discount <rate>', 'Source discount rate in PPM')
@@ -739,13 +740,14 @@ prog
739
740
  .action((args, options, logger) => {
740
741
  return new Promise(async (resolve, reject) => {
741
742
  try {
742
- return routing.adjustFees({
743
+ return routing.manageFees({
743
744
  logger,
744
745
  cltv_delta: options.setCltvDelta,
745
746
  fee_rate: options.setFeeRate,
746
747
  fs: {getFile: readFile},
747
748
  inbound_rate_discount: options.setInboundRateDiscount,
748
749
  lnd: (await lnd.authenticatedLnd({logger, node: options.node})).lnd,
750
+ repeat_interval_ms: options.repeatIntervalMs || undefined,
749
751
  to: flatten([options.to].filter(n => !!n)),
750
752
  },
751
753
  responses.returnObject({logger, reject, resolve, table: 'rows'}));
@@ -1514,6 +1516,10 @@ prog
1514
1516
  // Pay a payment request, probing first
1515
1517
  .command('pay', 'Pay a payment request, probing first')
1516
1518
  .help('Blinded path requests are probed to the introduction node, 1 path')
1519
+ .help('--out can take a tag to make the first hop through any tagged peer')
1520
+ .help('--out-filter vars: CAPACITY/HEIGHTS/INBOUND_LIQUIDITY')
1521
+ .help('--out-filter vars: OUTBOUND_LIQUIDITY/PENDING_PAYMENTS')
1522
+ .help('--out-filter vars: INBOUND_BASE_FEE/INBOUND_FEE_RATE')
1517
1523
  .argument('<request>', 'Payment Request')
1518
1524
  .option('--avoid <avoid>', 'Avoid forwarding via node/chan/tag', REPEATABLE)
1519
1525
  .option('--avoid-high-fee-routes', 'Avoid evaluating too-high fee routes')
@@ -1523,7 +1529,8 @@ prog
1523
1529
  .option('--message <message>', 'Attach text message to payment')
1524
1530
  .option('--no-color', 'Mute all colors')
1525
1531
  .option('--node <node_name>', 'Node to use to pay payment request')
1526
- .option('--out <public_key>', 'Make first hop through peer', REPEATABLE)
1532
+ .option('--out <peer_or_tag>', 'Make first hop through peer/tag', REPEATABLE)
1533
+ .option('--out-filter <out_filter>', 'Filter outbound peer/tag', REPEATABLE)
1527
1534
  .action((args, options, logger) => {
1528
1535
  return new Promise(async (resolve, reject) => {
1529
1536
  try {
@@ -1542,6 +1549,7 @@ prog
1542
1549
  max_paths: options.maxPaths,
1543
1550
  message: options.message,
1544
1551
  out: flatten([options.out].filter(n => !!n)),
1552
+ out_filters: flatten([options.outFilter].filter(n => !!n)),
1545
1553
  request: args.request,
1546
1554
  },
1547
1555
  responses.returnObject({logger, reject, resolve}));
@@ -1634,6 +1642,10 @@ prog
1634
1642
  .command('probe', 'Check if a payment request is sendable')
1635
1643
  .help('Simulate paying a payment request without actually paying it')
1636
1644
  .help('Blinded path requests are probed to the introduction node, 1 path')
1645
+ .help('--out can take a tag to make the first hop through any tagged peer')
1646
+ .help('--out-filter vars: CAPACITY/HEIGHTS/INBOUND_LIQUIDITY')
1647
+ .help('--out-filter vars: OUTBOUND_LIQUIDITY/PENDING_PAYMENTS')
1648
+ .help('--out-filter vars: INBOUND_BASE_FEE/INBOUND_FEE_RATE')
1637
1649
  .argument('<to>', 'Payment request or node public key')
1638
1650
  .argument('[amount]', 'Amount to probe, default: request amount')
1639
1651
  .option('--avoid <avoid>', 'Avoid forwarding via node/chan/tag', REPEATABLE)
@@ -1644,7 +1656,8 @@ prog
1644
1656
  .option('--max-paths <max>', 'Maximum paths to use for find-max', INT, 1)
1645
1657
  .option('--no-color', 'Mute all colors')
1646
1658
  .option('--node <node_name>', 'Node to use for payment request check')
1647
- .option('--out <public_key>', 'Make first hop through peer', REPEATABLE)
1659
+ .option('--out <peer_or_tag>', 'Make first hop through peer/tag', REPEATABLE)
1660
+ .option('--out-filter <out_filter>', 'Filter outbound peer/tag', REPEATABLE)
1648
1661
  .action((args, options, logger) => {
1649
1662
  return new Promise(async (resolve, reject) => {
1650
1663
  try {
@@ -1664,6 +1677,7 @@ prog
1664
1677
  max_fee: options.maxFee,
1665
1678
  max_paths: options.maxPaths,
1666
1679
  out: flatten([options.out].filter(n => !!n)),
1680
+ out_filters: flatten([options.outFilter].filter(n => !!n)),
1667
1681
  request: args.to.length !== 66 ? args.to : undefined,
1668
1682
  tokens: args.amount || undefined,
1669
1683
  },
@@ -1693,6 +1707,8 @@ prog
1693
1707
  .help('--in-filter/--out-filter vars: OUTBOUND_LIQUIDITY/PENDING_PAYMENTS')
1694
1708
  .help('--in-filter/--out-filter vars: INBOUND_BASE_FEE/INBOUND_FEE_RATE')
1695
1709
  .help('--out increases the inbound liquidity with a specific peer/tag')
1710
+ .help('--repeat-interval-ms vars: s, min, h, d, w: 1*h repeats hourly')
1711
+ .help('--repeat-interval-ms var FAILURES_COUNT: count of failures in a row')
1696
1712
  .option('--amount <amount>', 'Maximum amount to rebalance')
1697
1713
  .option('--avoid <pubkey_or_chanid>', 'Avoid forwarding through', REPEATABLE)
1698
1714
  .option('--avoid-append', 'Formula for appending failing edges to list')
@@ -1709,6 +1725,7 @@ prog
1709
1725
  .option('--out <pubkey_or_alias>', 'Route out through a specific peer')
1710
1726
  .option('--out-filter <out_filter>', 'Filter outbound peer/tag', REPEATABLE)
1711
1727
  .option('--out-target-inbound <amount>', 'Balance up to inbound amount')
1728
+ .option('--repeat-interval-ms <interval>', 'Repeat rebalance after interval')
1712
1729
  .action((args, options, logger) => {
1713
1730
  return new Promise(async (resolve, reject) => {
1714
1731
  try {
@@ -1729,6 +1746,7 @@ prog
1729
1746
  out_filters: flatten([options.outFilter].filter(n => !!n)),
1730
1747
  out_inbound: options.outTargetInbound,
1731
1748
  out_through: options.out || undefined,
1749
+ repeat_interval_ms: options.repeatIntervalMs || undefined,
1732
1750
  timeout_minutes: options.minutes || undefined,
1733
1751
  },
1734
1752
  responses.returnObject({exit, logger, reject, resolve}));
package/chain/index.js CHANGED
@@ -6,7 +6,6 @@ const getDepositAddress = require('./get_deposit_address');
6
6
  const getMempoolSize = require('./get_mempool_size');
7
7
  const getRawTransaction = require('./get_raw_transaction');
8
8
  const getUtxos = require('./get_utxos');
9
- const outputScriptForAddress = require('./output_script_for_address');
10
9
  const recoverP2pk = require('./recover_p2pk');
11
10
  const splitUtxos = require('./split_utxos');
12
11
 
@@ -19,7 +18,6 @@ module.exports = {
19
18
  getMempoolSize,
20
19
  getRawTransaction,
21
20
  getUtxos,
22
- outputScriptForAddress,
23
21
  recoverP2pk,
24
22
  splitUtxos,
25
23
  };
@@ -1,40 +1,39 @@
1
1
  const {createHash} = require('crypto');
2
2
 
3
3
  const asyncAuto = require('async/auto');
4
- const bitcoinjsLib = require('bitcoinjs-lib');
5
4
  const {broadcastChainTransaction} = require('ln-service');
5
+ const {componentsOfTransaction} = require('@alexbosworth/blockchain');
6
6
  const {createChainAddress} = require('ln-service');
7
- const {decode} = require('bip66');
8
7
  const {getChainFeeRate} = require('ln-service');
9
8
  const {getIdentity} = require('ln-service');
10
9
  const {getNetwork} = require('ln-sync');
10
+ const {idForTransaction} = require('@alexbosworth/blockchain');
11
+ const {outputScriptForAddress} = require('@alexbosworth/blockchain');
11
12
  const {returnResult} = require('asyncjs-util');
13
+ const {scriptElementsAsScript} = require('@alexbosworth/blockchain');
12
14
  const {signBytes} = require('ln-service');
15
+ const {sizeOfTransaction} = require('@alexbosworth/blockchain');
16
+ const {transactionFromComponents} = require('@alexbosworth/blockchain');
13
17
 
14
18
  const getRawTransaction = require('./get_raw_transaction');
15
19
 
16
20
  const {ceil} = Math;
17
- const {compile} = bitcoinjsLib.script;
18
21
  const {concat} = Buffer;
19
22
  const description = 'bos recover p2pk node identity key funds';
20
- const {encode} = bitcoinjsLib.script.signature;
21
23
  const estimatedSignatureSize = 73;
22
24
  const format = 'p2wpkh';
23
- const {fromHex} = bitcoinjsLib.Transaction;
24
25
  const hashFlag = Buffer.from('01000000', 'hex');
25
26
  const hexAsBuffer = hex => Buffer.from(hex, 'hex');
26
- const inputIndex = 0;
27
27
  const inputSequence = 0;
28
28
  const isHash = n => !!n && /^[0-9A-F]{64}$/i.test(n);
29
- const networkNames = {btc: 'bitcoin', btctestnet: 'testnet'};
30
- const {networks} = bitcoinjsLib;
29
+ const locktime = 0;
31
30
  const nodeIdentityKeyFamily = 6;
32
31
  const nodeIdentityKeyIndex = 0;
33
32
  const OP_CHECKSIG = 172;
34
33
  const sha256 = n => createHash('sha256').update(n).digest().toString('hex');
35
- const slicePoint = r => r.length === 33 ? r.slice(1) : r;
36
- const {toOutputScript} = bitcoinjsLib.address;
37
- const {Transaction} = bitcoinjsLib;
34
+ const sigHashType = Buffer.from('01', 'hex');
35
+ const unsignedScript = '';
36
+ const version = 1;
38
37
 
39
38
  /** Recover funds sent to a P2PK using the node identity key
40
39
 
@@ -97,14 +96,16 @@ module.exports = ({id, lnd, request, vout}, cbk) => {
97
96
 
98
97
  // Derive the transaction details
99
98
  output: ['getIdentity', 'getTx', ({getIdentity, getTx}, cbk) => {
100
- const tx = fromHex(getTx.transaction);
99
+ const {transaction} = getTx;
101
100
 
102
101
  // Make sure the tx data matches the input id
103
- if (tx.getId() !== id) {
102
+ if (idForTransaction({transaction}).id !== id) {
104
103
  return cbk([503, 'ExpectedTransactionIdToMatchTxDataHashToRecover']);
105
104
  }
106
105
 
107
- const output = tx.outs[vout];
106
+ const {outputs} = componentsOfTransaction({transaction});
107
+
108
+ const output = outputs[vout];
108
109
 
109
110
  // Make sure the output exists
110
111
  if (!output) {
@@ -113,14 +114,16 @@ module.exports = ({id, lnd, request, vout}, cbk) => {
113
114
 
114
115
  const identityKey = hexAsBuffer(getIdentity.public_key);
115
116
 
116
- const expectedScript = compile([identityKey, OP_CHECKSIG]);
117
+ const expected = scriptElementsAsScript({
118
+ elements: [identityKey, OP_CHECKSIG],
119
+ });
117
120
 
118
121
  // Make sure that the output pays to the node identity key
119
- if (!output.script.equals(expectedScript)) {
122
+ if (output.script !== expected.script) {
120
123
  return cbk([503, 'ExpectedOutputPayingToNodeIdentityPublicKey']);
121
124
  }
122
125
 
123
- return cbk(null, {script: output.script, tokens: output.value});
126
+ return cbk(null, {script: output.script, tokens: output.tokens});
124
127
  }],
125
128
 
126
129
  // Create a recovery address
@@ -128,45 +131,73 @@ module.exports = ({id, lnd, request, vout}, cbk) => {
128
131
  return createChainAddress({format, lnd}, cbk);
129
132
  }],
130
133
 
131
- // Make the transaction to sign
132
- txToSign: [
134
+ // Derive the output script of the recovery address
135
+ recoveryScript: [
133
136
  'createAddress',
134
- 'getFee',
135
137
  'getNetwork',
136
- 'output',
137
- ({createAddress, getFee, getNetwork, output}, cbk) =>
138
+ ({createAddress, getNetwork}, cbk) =>
138
139
  {
139
- const network = networks[networkNames[getNetwork.network]];
140
- const tx = new Transaction();
141
-
142
- const scriptPubKey = toOutputScript(createAddress.address, network);
140
+ try {
141
+ const {script} = outputScriptForAddress({
142
+ address: createAddress.address,
143
+ network: getNetwork.network,
144
+ });
145
+
146
+ return cbk(null, {script});
147
+ } catch (err) {
148
+ return cbk([503, 'FailedToParseRecoveryAddress', {err}]);
149
+ }
150
+ }],
143
151
 
144
- tx.addInput(hexAsBuffer(id).reverse(), vout, inputSequence);
145
- tx.addOutput(scriptPubKey, output.tokens);
152
+ // Derive the sweep output, less the amount needed to pay for chain fees
153
+ sweep: [
154
+ 'getFee',
155
+ 'output',
156
+ 'recoveryScript',
157
+ ({getFee, output, recoveryScript}, cbk) =>
158
+ {
159
+ const {script} = recoveryScript;
160
+
161
+ const unsigned = transactionFromComponents({
162
+ locktime,
163
+ version,
164
+ inputs: [{
165
+ id,
166
+ vout,
167
+ script: unsignedScript,
168
+ sequence: inputSequence,
169
+ }],
170
+ outputs: [{script, tokens: output.tokens}],
171
+ });
146
172
 
147
- // There is only one output on the sweep transaction
148
- const [out] = tx.outs;
173
+ const {vsize} = sizeOfTransaction(unsigned);
149
174
 
150
175
  // Include the prospective signature in the tx total weight
151
- const vbytes = tx.virtualSize() + estimatedSignatureSize;
176
+ const vbytes = vsize + estimatedSignatureSize;
152
177
 
153
178
  // Reduce the sweep value by the amount needed to pay for chain fees
154
- out.value -= ceil(vbytes * getFee.tokens_per_vbyte);
179
+ const tokens = output.tokens - ceil(vbytes * getFee.tokens_per_vbyte);
155
180
 
156
- return cbk(null, tx);
181
+ return cbk(null, {script, tokens});
157
182
  }],
158
183
 
159
184
  // Derive the preimage to use for signing
160
- preimage: ['output', 'txToSign', ({output, txToSign}, cbk) => {
161
- const cloneTx = txToSign.clone();
162
-
163
- const [input] = cloneTx.ins;
164
-
185
+ preimage: ['output', 'sweep', ({output, sweep}, cbk) => {
165
186
  // When signing, the input to sign is set to the previous output script
166
- input.script = output.script;
187
+ const {transaction} = transactionFromComponents({
188
+ locktime,
189
+ version,
190
+ inputs: [{
191
+ id,
192
+ vout,
193
+ script: output.script,
194
+ sequence: inputSequence,
195
+ }],
196
+ outputs: [sweep],
197
+ });
167
198
 
168
199
  // The bytes to sign are the tx itself plus the signature hash flag
169
- return cbk(null, concat([cloneTx.toBuffer(), hashFlag]));
200
+ return cbk(null, concat([hexAsBuffer(transaction), hashFlag]));
170
201
  }],
171
202
 
172
203
  // Give the preimage to signer, hashed once - signBytes does 2nd SHA hash
@@ -181,17 +212,18 @@ module.exports = ({id, lnd, request, vout}, cbk) => {
181
212
  }],
182
213
 
183
214
  // Put together the signature with the transaction
184
- signedTx: ['getSig', 'txToSign', ({getSig, txToSign}, cbk) => {
185
- const {r, s} = decode(hexAsBuffer(getSig.signature));
186
-
187
- const rValue = slicePoint(r);
188
-
189
- // Convert the signature from sign bytes to a chain signature
190
- const scriptSig = encode(concat([rValue, s]), Transaction.SIGHASH_ALL);
191
-
192
- txToSign.setInputScript(inputIndex, compile([scriptSig]));
193
-
194
- return cbk(null, txToSign);
215
+ signedTx: ['getSig', 'sweep', ({getSig, sweep}, cbk) => {
216
+ // A chain signature is the DER signature followed by the sighash type
217
+ const signature = concat([hexAsBuffer(getSig.signature), sigHashType]);
218
+
219
+ const {script} = scriptElementsAsScript({elements: [signature]});
220
+
221
+ return cbk(null, transactionFromComponents({
222
+ locktime,
223
+ version,
224
+ inputs: [{id, script, vout, sequence: inputSequence}],
225
+ outputs: [sweep],
226
+ }));
195
227
  }],
196
228
 
197
229
  // Broadcast the signed transaction
@@ -199,7 +231,7 @@ module.exports = ({id, lnd, request, vout}, cbk) => {
199
231
  return broadcastChainTransaction({
200
232
  description,
201
233
  lnd,
202
- transaction: signedTx.toHex(),
234
+ transaction: signedTx.transaction,
203
235
  },
204
236
  cbk);
205
237
  }],
@@ -214,7 +246,7 @@ module.exports = ({id, lnd, request, vout}, cbk) => {
214
246
  return cbk(null, {
215
247
  recovering: output.tokens,
216
248
  recovering_to: createAddress.address,
217
- transaction_id: signedTx.getId(),
249
+ transaction_id: idForTransaction(signedTx).id,
218
250
  });
219
251
  }],
220
252
  },
@@ -1,14 +1,12 @@
1
1
  const {stringify} = require('querystring');
2
2
 
3
- const {AbortController} = require('abort-controller');
4
-
5
3
  const encodeQs = qs => !qs ? '' : '?' + stringify(qs);
6
4
  const timeoutSignals = new WeakMap();
7
5
 
8
6
  /** Derive a request function that uses fetch to simulate request behavior
9
7
 
10
8
  {
11
- fetch: <Node Fetch Function>
9
+ fetch: <Fetch Function>
12
10
  }
13
11
 
14
12
  @returns
@@ -1,4 +1,10 @@
1
1
  {
2
+ "durationUnits": [
3
+ {"ms": 86400000, "unit": "day"},
4
+ {"ms": 3600000, "unit": "hour"},
5
+ {"ms": 60000, "unit": "minute"},
6
+ {"ms": 1000, "unit": "second"}
7
+ ],
2
8
  "parseErrors": {
3
9
  "#DIV/0!": "CannotDivideByZeroInFormula",
4
10
  "#ERROR!": "FailedToParseFormula",
@@ -0,0 +1,34 @@
1
+ const {durationUnits} = require('./constants');
2
+
3
+ const {floor} = Math;
4
+ const maxUnits = 2;
5
+ const plural = (count, unit) => `${count} ${unit}${count === 1 ? '' : 's'}`;
6
+
7
+ /** Format a duration for display, using the two largest units of time
8
+
9
+ {
10
+ ms: <Duration Milliseconds Number>
11
+ }
12
+
13
+ @returns
14
+ {
15
+ display: <Display Formatted Duration String>
16
+ }
17
+ */
18
+ module.exports = ({ms}) => {
19
+ // Take whole counts of each unit out of the duration, largest unit first
20
+ const {parts, remaining} = durationUnits.reduce((sum, unit) => {
21
+ const count = floor(sum.remaining / unit.ms);
22
+
23
+ return {
24
+ parts: !count ? sum.parts : sum.parts.concat(plural(count, unit.unit)),
25
+ remaining: sum.remaining - count * unit.ms,
26
+ };
27
+ },
28
+ {parts: [], remaining: ms});
29
+
30
+ // Leftover milliseconds are shown when present or when there is nothing else
31
+ const leftover = !remaining && !!parts.length ? [] : [`${remaining} ms`];
32
+
33
+ return {display: parts.concat(leftover).slice(0, maxUnits).join(' ')};
34
+ };
package/display/index.js CHANGED
@@ -2,6 +2,7 @@ const chartAliasForPeer = require('./chart_alias_for_peer');
2
2
  const describeConfidence = require('./describe_confidence');
3
3
  const describeRoute = require('./describe_route');
4
4
  const describeRoutingFailure = require('./describe_routing_failure');
5
+ const formatDuration = require('./format_duration');
5
6
  const formatFeeRate = require('./format_fee_rate');
6
7
  const getIcons = require('./get_icons');
7
8
  const isMatchingFilters = require('./is_matching_filters');
@@ -13,6 +14,7 @@ module.exports = {
13
14
  describeConfidence,
14
15
  describeRoute,
15
16
  describeRoutingFailure,
17
+ formatDuration,
16
18
  formatFeeRate,
17
19
  getIcons,
18
20
  isMatchingFilters,
@@ -40,7 +40,7 @@ const tokensAsMillitokens = tok => (BigInt(tok) * BigInt(1e3)).toString();
40
40
  type: <Message To Final Destination Type Number String>
41
41
  value: <Message To Final Destination Raw Value Hex Encoded String>
42
42
  }]
43
- [outgoing_channel]: <Outgoing Channel Id String>
43
+ [outgoing_channels]: [<Outgoing Channel Id String>]
44
44
  [paths]: [{
45
45
  base_fee_mtokens: <Accumulated Base Fee Millitokens String>
46
46
  cltv_delta: <Accumulated CLTV Expiry Delta Number>
@@ -176,7 +176,7 @@ module.exports = (args, cbk) => {
176
176
  max_fee_mtokens: strictMaxFee || undefined,
177
177
  max_timeout_height: args.max_timeout_height,
178
178
  messages: args.messages,
179
- outgoing_channel: args.outgoing_channel,
179
+ outgoing_channels: args.outgoing_channels,
180
180
  path_timeout_ms: pathTimeoutMs,
181
181
  paths: args.paths,
182
182
  payment: args.payment,