balanceofsatoshis 25.6.2 → 26.0.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,43 @@
1
1
  # Versions
2
2
 
3
+ ## 26.0.0
4
+
5
+ - `swap-api-key`: Add `--max-fee` to limit what is paid for an API key
6
+ - `swap-in`: Use the shared address swap in protocol of the swap service,
7
+ swapping in a deposit of the exact amount once it has 3 confirmations
8
+ - `swap-in`: Add `--max-fee-rate` to limit the swap fee in parts per million
9
+ - `swap-in`, `swap-out`: Add `--max-api-key-fee` to limit what is paid for a
10
+ swap service API key
11
+ - `swap-out`: Use the Taproot MuSig2 swap out protocol of the swap service,
12
+ like `swap` does
13
+ - `swap-out`: Add `--out` to select the peer or tag to increase inbound
14
+ liquidity from, with `--out-filter` formulas like `rebalance`
15
+
16
+ ### Breaking Changes
17
+
18
+ - `increase-inbound-liquidity`: Rename command to `swap-out`
19
+ - `swap-in`: `--recovery` takes a shared address secret, not old recovery codes
20
+ - `swap-in`: `--refund-address` with `--recovery` refunds expired deposits
21
+ and unpaid swaps that timed out
22
+ - `swap-in`: Remove `--test-refund`
23
+ - `swap-in`: Use 25.7.0 to refund swaps in started with earlier versions
24
+ - `swap-out`: `--recovery` takes the restart code of the new swap protocol,
25
+ which `swap` also accepts, not the previous recovery code
26
+ - `swap-out`: Remove `--max-hours`, `--show-raw-recovery`, `--spend-address`
27
+ and `--spend-amount`
28
+ - `swap-out`: Rename `--with` to `--out`
29
+
30
+ ## 25.7.0
31
+
32
+ - `accounting`: Add `lightning-fees` category to report the routing fees paid
33
+ on payments
34
+ - `accounting`: Keep the payments of recent blocks in `chain_blocks.json`, so
35
+ that later runs only fetch the blocks that are new
36
+ - `accounting`: Pass `--node` more than once to get one report of several
37
+ nodes, with their records together in order of date
38
+ - `fund`: Add `--fee-rate-precise` to set a fee rate per 1000 vbytes
39
+ - `fund`: `MAX` amounts send as much as possible without overpaying the fee
40
+
3
41
  ## 25.6.2
4
42
 
5
43
  - `telegram`: Fix invoices settled before the bot started being reported as new
package/README.md CHANGED
@@ -123,9 +123,6 @@ bos inbound-liquidity
123
123
  # Enforce rules on inbound channels
124
124
  bos inbound-channel-rules
125
125
 
126
- # Increase inbound liquidity to the node
127
- bos increase-inbound-liquidity
128
-
129
126
  # Increase the outbound liquidity of the node
130
127
  bos increase-outbound-liquidity
131
128
 
@@ -171,6 +168,12 @@ bos server
171
168
  # Stream a payment to a lnurl or lightning address in a series of payments
172
169
  bos stream "amount" --lnurl "lnurl/lightning_address"
173
170
 
171
+ # Trade on-chain funds for off-chain funds to increase outbound liquidity
172
+ bos swap-in
173
+
174
+ # Trade off-chain funds for on-chain funds to increase inbound liquidity
175
+ bos swap-out
176
+
174
177
  # Connect up to a Telegram bot
175
178
  bos telegram
176
179
 
@@ -305,6 +308,9 @@ You can also add a JSON file to your .bos directory: config.json, add
305
308
 
306
309
  `bos` keeps its files in `~/.bos`, or in `BOS_DATA_PATH` when that is set:
307
310
 
311
+ - `chain_blocks.json`: the payments in the last few days of blocks that
312
+ `bos accounting` derives prices from, kept so that later runs only fetch
313
+ the blocks that are new. The file can be deleted
308
314
  - `chain_prices.json`: USD prices of BTC that `bos accounting` derived from
309
315
  chain data, kept so that a day is only derived once. Prices are derived
310
316
  this way when no `--rate-provider` is given, and the prices of the days
@@ -614,6 +620,7 @@ Formula amounts are supported in the following commands:
614
620
  - `probe`
615
621
  - `rebalance`
616
622
  - `send`
623
+ - `swap-out`
617
624
 
618
625
  When passing an amount you can pass a formula expression, and the following variables are
619
626
  defined:
@@ -673,6 +680,25 @@ Example:
673
680
  bos send <key> --amount "1*usd"
674
681
  ```
675
682
 
683
+ #### `swap-out`
684
+
685
+ Swap out defines variables for `--out-filter`:
686
+
687
+ - `capacity`: The total capacity with the peer
688
+ - `heights`: The set of heights of the channels with the peer
689
+ - `inbound_base_fee`: The base fee the peer is charging
690
+ - `inbound_fee_rate`: The fee rate the peer is charging
691
+ - `inbound_liquidity`: The inbound liquidity with the peer
692
+ - `outbound_liquidity`: The outbound liquidity with the peer
693
+ - `pending_payments`: The number of pending payments
694
+
695
+ Example:
696
+
697
+ ```shell
698
+ // Swap out 1,000,000 with a peer of the tag that has under 2,000,000 inbound
699
+ bos swap-out --amount "1*m" --out "tag" --out-filter "inbound_liquidity < 2*m"
700
+ ```
701
+
676
702
  #### `transfer`
677
703
 
678
704
  Transfer variables:
@@ -4,5 +4,6 @@
4
4
  "chain-sends": "chain_sends",
5
5
  "forwards": "forwards",
6
6
  "invoices": "invoices",
7
+ "lightning-fees": "lightning_fees",
7
8
  "payments": "payments"
8
9
  }
@@ -0,0 +1,30 @@
1
+ const {isFinite} = Number;
2
+ const {round} = Math;
3
+ const {sqrt} = Math;
4
+
5
+ /** Get the range of prices that the round dollar payments of a day point to
6
+
7
+ {
8
+ [dispersion]: <Peak Width Beyond Smoothing As Price Fraction Number>
9
+ price: <Price String>
10
+ }
11
+
12
+ @returns
13
+ {
14
+ [range]: <Low Price to High Price String>
15
+ }
16
+ */
17
+ module.exports = ({dispersion, price}) => {
18
+ // Exit early when there is no width to describe
19
+ if (!isFinite(dispersion)) {
20
+ return {};
21
+ }
22
+
23
+ // The peak reaches the square root of the dispersion factor either side
24
+ const reach = sqrt(1 + dispersion);
25
+
26
+ const high = round(Number(price) * reach);
27
+ const low = round(Number(price) / reach);
28
+
29
+ return {range: `${low} to ${high}`};
30
+ };
@@ -0,0 +1,78 @@
1
+ const chainPriceRange = require('./chain_price_range');
2
+ const {chainPriceFlagNotes} = require('./constants');
3
+
4
+ const {floor} = Math;
5
+ const {keys} = Object;
6
+ const msPerDay = 1000 * 60 * 60 * 24;
7
+
8
+ /** Make a logger that describes deriving prices from chain data
9
+
10
+ {
11
+ [logger]: <Winston Logger Object>
12
+ }
13
+
14
+ @returns
15
+ {
16
+ [logger]: {
17
+ error: <Error Function>
18
+ info: <Info Function>
19
+ }
20
+ }
21
+ */
22
+ module.exports = ({logger}) => {
23
+ // Exit early when there is no logger to describe progress with
24
+ if (!logger) {
25
+ return {};
26
+ }
27
+
28
+ return {
29
+ logger: {
30
+ error: err => logger.error(err),
31
+ info: line => {
32
+ // Exit early describing fetching blocks as calculating prices
33
+ if (!!line && !!line.fetching_blocks) {
34
+ const {blocks, date} = line.fetching_blocks;
35
+
36
+ const days = floor((Date.now() - Date.parse(date)) / msPerDay);
37
+
38
+ const isToday = days === 0;
39
+
40
+ return logger.info({
41
+ calculating_prices_from_blocks: {
42
+ blocks,
43
+ day: date,
44
+ days_out_of_date: !isNaN(days) && !isToday ? days : undefined,
45
+ },
46
+ });
47
+ }
48
+
49
+ // Exit early when the line is not a price derived from chain data
50
+ if (!line || !line.chain_price || !line.chain_price.price) {
51
+ return logger.info(line);
52
+ }
53
+
54
+ const price = line.chain_price;
55
+
56
+ const flags = keys(chainPriceFlagNotes).filter(flag => !!price[flag]);
57
+
58
+ const note = flags.map(flag => chainPriceFlagNotes[flag]).join(', ');
59
+
60
+ // Only a price from a full day of blocks has the measures behind it
61
+ const isFullDay = price.outputs !== undefined;
62
+
63
+ // Keys are in reading order, the price first and then its evidence
64
+ return logger.info({
65
+ chain_price: {
66
+ date: price.date,
67
+ price: price.price,
68
+ price_range: isFullDay ? chainPriceRange(price).range : undefined,
69
+ blocks: isFullDay ? price.blocks : undefined,
70
+ payment_outputs: isFullDay ? price.outputs : undefined,
71
+ round_dollar_payments: isFullDay ? price.payments : undefined,
72
+ note: note || undefined,
73
+ },
74
+ });
75
+ },
76
+ },
77
+ };
78
+ };
@@ -1,6 +1,14 @@
1
1
  {
2
+ "chainBlocksFile": "chain_blocks.json",
3
+ "chainPriceFlagNotes": {
4
+ "is_provisional": "provisional, the day is not over yet",
5
+ "is_sample": "rough estimate from a sample of the blocks",
6
+ "is_unsettled": "the estimate was still shifting when refining stopped",
7
+ "is_widened": "found outside the expected price range"
8
+ },
2
9
  "chainPricesFile": "chain_prices.json",
3
10
  "coinbaseProvider": "coinbase",
11
+ "dayFormat": "YYYY-MM-DD",
4
12
  "defaultCurrency": "BTC",
5
13
  "defaultFiat": "USD",
6
14
  "failedRequestErrors": ["UnexpectedErrorGettingCoinbaseCandles"],
@@ -38,5 +46,14 @@
38
46
  "FailedToReplacePricesFile",
39
47
  "FailedToWritePricesFile",
40
48
  "UnsupportedPricesFileVersion"
49
+ ],
50
+ "summaryHeadings": [
51
+ "Total",
52
+ "Asset",
53
+ "Report Date",
54
+ "Total Fiat",
55
+ "Category",
56
+ "Start Date",
57
+ "End Date"
41
58
  ]
42
59
  }
@@ -1,12 +1,20 @@
1
1
  const asyncAuto = require('async/auto');
2
+ const asyncify = require('async/asyncify');
3
+ const asyncMap = require('async/map');
4
+ const asyncMapSeries = require('async/mapSeries');
2
5
  const {chainRates} = require('@alexbosworth/fiat');
3
6
  const {getAccountingReport} = require('ln-accounting');
4
7
  const {getNetwork} = require('ln-sync');
8
+ const {harmonize} = require('ln-accounting');
9
+ const moment = require('moment');
5
10
  const {returnResult} = require('asyncjs-util');
6
11
 
7
12
  const categories = require('./accounting_categories');
13
+ const {chainBlocksFile} = require('./constants');
8
14
  const {chainPricesFile} = require('./constants');
15
+ const chainPricesLogger = require('./chain_prices_logger');
9
16
  const {coinbaseProvider} = require('./constants');
17
+ const {dayFormat} = require('./constants');
10
18
  const {defaultCurrency} = require('./constants');
11
19
  const {defaultFiat} = require('./constants');
12
20
  const {fsMethods} = require('./constants');
@@ -14,18 +22,20 @@ const getRecordRate = require('./get_record_rate');
14
22
  const guardChainRate = require('./guard_chain_rate');
15
23
  const {homePath} = require('./../storage');
16
24
  const {mainnet} = require('./constants');
17
- const {monthNumbers} = require('./constants');
18
- const {monthOffset} = require('./constants');
19
- const {notFoundIndex} = require('./constants');
20
25
  const rangeForDate = require('./range_for_date');
26
+ const {sortBy} = require('./../arrays');
27
+ const {summaryHeadings} = require('./constants');
21
28
  const tableRowsFromCsv = require('./table_rows_from_csv');
22
29
 
23
30
  const assetType = 'BTC';
24
- const currentDate = new Date().toISOString();
31
+ const dayOf = n => moment.utc(n).format(dayFormat);
25
32
  const empty = '';
33
+ const flatten = arr => [].concat(...arr);
34
+ const {isArray} = Array;
35
+ const {keys} = Object;
36
+ const lastDayBefore = n => moment.utc(n).subtract(1, 'ms').format(dayFormat);
26
37
  const round = n => parseFloat(n).toFixed(2);
27
38
  const sumOf = arr => arr.reduce((sum, n) => sum + n, 0);
28
- const summaryHeadings = ['Total', 'Asset', 'Report Date', 'Total Fiat'];
29
39
 
30
40
  /** Get an accounting report
31
41
 
@@ -42,10 +52,9 @@ const summaryHeadings = ['Total', 'Asset', 'Report Date', 'Total Fiat'];
42
52
  }
43
53
  [is_csv]: <Return CSV Output Bool>
44
54
  [is_fiat_disabled]: <Omit Fiat Conversion Bool>
45
- lnd: <Authenticated LND API Object>
55
+ lnds: [<Authenticated LND API Object>]
46
56
  [logger]: <Winston Logger Object>
47
57
  [month]: <Month for Report String>
48
- [node]: <Node Name String>
49
58
  [rate_provider]: <Rate Provider String>
50
59
  request: <Request Function>
51
60
  [year]: <Year for Report String>
@@ -65,7 +74,9 @@ module.exports = (args, cbk) => {
65
74
  const {fs} = args;
66
75
 
67
76
  if (!args.category || !categories[args.category]) {
68
- return cbk([400, 'ExpectedKnownAccountingRecordsCategory']);
77
+ return cbk([400, 'ExpectedKnownAccountingRecordsCategory', {
78
+ categories: keys(categories),
79
+ }]);
69
80
  }
70
81
 
71
82
  if (!!args.date && !args.month) {
@@ -76,8 +87,8 @@ module.exports = (args, cbk) => {
76
87
  return cbk([400, 'ExpectedFileSystemMethodsToGetAccountingReport']);
77
88
  }
78
89
 
79
- if (!args.lnd) {
80
- return cbk([400, 'ExpectedAuthenticatedLndToGetAccountingReport']);
90
+ if (!isArray(args.lnds) || !args.lnds.length) {
91
+ return cbk([400, 'ExpectedAuthenticatedLndsToGetAccountingReport']);
81
92
  }
82
93
 
83
94
  if (!args.request) {
@@ -88,34 +99,37 @@ module.exports = (args, cbk) => {
88
99
  },
89
100
 
90
101
  // Get date range
91
- dateRange: ['validate', ({}, cbk) => {
92
- const range = (() => {
93
- try {
94
- return {
95
- value: rangeForDate({
96
- date: args.date,
97
- month: args.month,
98
- year: args.year,
99
- }),
100
- };
101
- } catch (err) {
102
- return {err};
103
- }
104
- })();
105
-
106
- // Exit early when the date range cannot be determined
107
- if (!!range.err) {
108
- return cbk([400, range.err.message]);
102
+ dateRange: ['validate', asyncify(() => {
103
+ try {
104
+ return rangeForDate({
105
+ date: args.date,
106
+ month: args.month,
107
+ year: args.year,
108
+ });
109
+ } catch (err) {
110
+ throw [400, err.message];
109
111
  }
112
+ })],
110
113
 
111
- return cbk(null, range.value);
114
+ // Get the network names of the nodes
115
+ getNetworks: ['validate', ({}, cbk) => {
116
+ return asyncMap(args.lnds, (lnd, cbk) => getNetwork({lnd}, cbk), cbk);
112
117
  }],
113
118
 
114
- // Get the network name
115
- getNetwork: ['validate', ({}, cbk) => getNetwork({lnd: args.lnd}, cbk)],
119
+ // The network that the nodes are on
120
+ network: ['getNetworks', ({getNetworks}, cbk) => {
121
+ const [{network}] = getNetworks;
122
+
123
+ // Exit early when the nodes are not all on the same network
124
+ if (getNetworks.some(n => n.network !== network)) {
125
+ return cbk([400, 'ExpectedSameNetworkNodesForAccountingReport']);
126
+ }
127
+
128
+ return cbk(null, network);
129
+ }],
116
130
 
117
131
  // Make the method that gets chain prices, used without a rate provider
118
- chain: ['getNetwork', ({getNetwork}, cbk) => {
132
+ chain: ['network', ({network}, cbk) => {
119
133
  // Exit early when fiat is disabled or a rate provider is specified
120
134
  if (!!args.is_fiat_disabled || !!args.rate_provider) {
121
135
  return cbk();
@@ -123,17 +137,23 @@ module.exports = (args, cbk) => {
123
137
 
124
138
  // Exit early when there are no chain prices for the network or no
125
139
  // file system to keep them in
126
- if (!args.fs || getNetwork.network !== mainnet) {
140
+ if (!args.fs || network !== mainnet) {
127
141
  return cbk();
128
142
  }
129
143
 
130
144
  // Progress is only logged when it cannot end up in CSV output
131
- const logger = !args.is_csv ? args.logger : undefined;
145
+ const {logger} = chainPricesLogger({
146
+ logger: !args.is_csv ? args.logger : undefined,
147
+ });
148
+
149
+ // Chain prices are derived through the first node
150
+ const [lnd] = args.lnds;
132
151
 
133
152
  const {getChainRate} = chainRates({
153
+ lnd,
134
154
  logger,
155
+ blocks_path: homePath({file: chainBlocksFile}).path,
135
156
  fs: args.fs,
136
- lnd: args.lnd,
137
157
  path: homePath({file: chainPricesFile}).path,
138
158
  });
139
159
 
@@ -165,51 +185,63 @@ module.exports = (args, cbk) => {
165
185
  });
166
186
  }],
167
187
 
168
- // Get accounting info
188
+ // Get the accounting records of each node, one node after another
169
189
  getAccounting: [
170
190
  'dateRange',
171
- 'getNetwork',
191
+ 'network',
172
192
  'rate',
173
- ({dateRange, getNetwork, rate}, cbk) =>
193
+ ({dateRange, network, rate}, cbk) =>
174
194
  {
175
- return getAccountingReport({
176
- after: dateRange.after,
177
- before: dateRange.before,
178
- category: categories[args.category],
179
- currency: args.currency || defaultCurrency,
180
- fiat: !args.is_fiat_disabled ? (args.fiat || defaultFiat) : null,
181
- lnd: args.lnd,
182
- network: getNetwork.network,
183
- rate: !!rate ? rate.rate : undefined,
184
- rate_provider: !rate ? args.rate_provider : undefined,
185
- request: args.request,
195
+ return asyncMapSeries(args.lnds, (lnd, cbk) => {
196
+ return getAccountingReport({
197
+ lnd,
198
+ network,
199
+ after: dateRange.after,
200
+ before: dateRange.before,
201
+ category: categories[args.category],
202
+ currency: args.currency || defaultCurrency,
203
+ fiat: !args.is_fiat_disabled ? (args.fiat || defaultFiat) : null,
204
+ rate: !!rate ? rate.rate : undefined,
205
+ rate_provider: !rate ? args.rate_provider : undefined,
206
+ request: args.request,
207
+ },
208
+ cbk);
186
209
  },
187
210
  cbk);
188
211
  }],
189
212
 
190
- // Convert the accounting CSV into rows for table display output
191
- accounting: ['getAccounting', ({getAccounting}, cbk) => {
192
- const csvType = `${categories[args.category]}_csv`;
213
+ // The records of all of the nodes in the category, in order of date
214
+ records: ['getAccounting', ({getAccounting}, cbk) => {
215
+ const category = categories[args.category];
216
+
217
+ const records = flatten(getAccounting.map(report => report[category]));
218
+
219
+ const {sorted} = sortBy({array: records, attribute: 'created_at'});
220
+
221
+ return cbk(null, sorted);
222
+ }],
223
+
224
+ // Convert the records into a CSV, or into rows for table display output
225
+ accounting: ['records', ({records}, cbk) => {
226
+ const {csv} = harmonize({records});
193
227
 
194
228
  // Exit early when a CSV dump is requested
195
229
  if (!!args.is_csv) {
196
- return cbk(null, getAccounting[csvType]);
230
+ return cbk(null, csv);
197
231
  }
198
232
 
199
- return tableRowsFromCsv({csv: getAccounting[csvType]}, cbk);
233
+ return tableRowsFromCsv({csv}, cbk);
200
234
  }],
201
235
 
202
236
  // Calculate total amounts
203
- total: ['getAccounting', ({getAccounting}, cbk) => {
237
+ total: ['records', ({records}, cbk) => {
204
238
  // Exit early when a CSV dump is requested
205
239
  if (!!args.is_csv) {
206
240
  return cbk();
207
241
  }
208
242
 
209
- const rows = getAccounting[categories[args.category]];
210
-
211
243
  // Token values are represented as amounts
212
- const tokens = sumOf(rows.map(n => n.amount));
244
+ const tokens = sumOf(records.map(n => n.amount));
213
245
 
214
246
  // Exit early when there is no fiat data
215
247
  if (!!args.is_fiat_disabled) {
@@ -217,13 +249,18 @@ module.exports = (args, cbk) => {
217
249
  }
218
250
 
219
251
  // Fiat values are represented as fiat amounts
220
- const fiat = round(sumOf(rows.map(n => n.fiat_amount)));
252
+ const fiat = round(sumOf(records.map(n => n.fiat_amount)));
221
253
 
222
254
  return cbk(null, {fiat, tokens});
223
255
  }],
224
256
 
225
257
  // Clean rows for display if necessary
226
- report: ['accounting', 'total',({accounting, total}, cbk) => {
258
+ report: [
259
+ 'accounting',
260
+ 'dateRange',
261
+ 'total',
262
+ ({accounting, dateRange, total}, cbk) =>
263
+ {
227
264
  // Exit early when there is no cleaning necessary
228
265
  if (!!args.is_csv) {
229
266
  return cbk(null, accounting);
@@ -249,7 +286,15 @@ module.exports = (args, cbk) => {
249
286
 
250
287
  const summary = [
251
288
  summaryHeadings,
252
- [total.tokens, assetType, currentDate, total.fiat],
289
+ [
290
+ total.tokens,
291
+ assetType,
292
+ new Date().toISOString(),
293
+ total.fiat,
294
+ args.category,
295
+ !dateRange.after ? empty : dayOf(dateRange.after),
296
+ !dateRange.before ? empty : lastDayBefore(dateRange.before),
297
+ ],
253
298
  ];
254
299
 
255
300
  return cbk(null, {rows, rows_summary: summary});
@@ -4,7 +4,9 @@ const {monthNumbers} = require('./constants');
4
4
  const {monthOffset} = require('./constants');
5
5
  const {notFoundIndex} = require('./constants');
6
6
 
7
- /** Get a before and after range
7
+ const isDay = n => /^\d+$/.test(n) && Number(n) > 0;
8
+
9
+ /** Get a before and after range
8
10
 
9
11
  {
10
12
  [date]: <Day of Month String>
@@ -17,8 +19,8 @@ const {notFoundIndex} = require('./constants');
17
19
 
18
20
  @returns
19
21
  {
20
- [after]: <After ISO 8601 Date String>
21
- [before]: <Before ISO 8601 Date String>
22
+ [after]: <Records Created On or After ISO 8601 Date String>
23
+ [before]: <Records Created Before ISO 8601 Date String>
22
24
  }
23
25
  */
24
26
  module.exports = ({date, month, year}) => {
@@ -32,12 +34,6 @@ module.exports = ({date, month, year}) => {
32
34
  after.year(year);
33
35
  }
34
36
 
35
- try {
36
- after.toISOString();
37
- } catch (err) {
38
- throw new Error('UnrecognizedFormatForAccountingYear');
39
- }
40
-
41
37
  const end = after.clone();
42
38
 
43
39
  if (!!month && monthNumbers.indexOf(month) !== notFoundIndex) {
@@ -46,8 +42,12 @@ module.exports = ({date, month, year}) => {
46
42
  [after, end].forEach(n => n.month(month));
47
43
  }
48
44
 
45
+ if (!!date && (!isDay(date) || Number(date) > after.daysInMonth())) {
46
+ throw new Error('ExpectedDayInMonthForAccountingDate');
47
+ }
48
+
49
49
  if (!!date) {
50
- [after, end].forEach(n => n.date(date));
50
+ [after, end].forEach(n => n.date(Number(date)));
51
51
  }
52
52
 
53
53
  if (!!date) {
@@ -58,13 +58,5 @@ module.exports = ({date, month, year}) => {
58
58
  end.add([after].length, 'years');
59
59
  }
60
60
 
61
- try {
62
- after.toISOString();
63
- } catch (err) {
64
- throw new Error('UnrecognizedFormatForAccountingMonth');
65
- }
66
-
67
- after.subtract([after].length, 'millisecond');
68
-
69
61
  return {after: after.toISOString(), before: end.toISOString()};
70
62
  };