balanceofsatoshis 11.22.2 → 11.24.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,6 +1,14 @@
1
1
  # Versions
2
2
 
3
- ## 11.22.2
3
+ ## 11.24.0
4
+
5
+ - `fees`: Add `--set-cltv-delta` to control the forwarding CLTV delta with peer
6
+
7
+ ## 11.23.0
8
+
9
+ - `balance`: `--detailed`: Support unconfirmed/invalid/conflicting balances
10
+
11
+ ## 11.22.4
4
12
 
5
13
  - `telegram`: Fix issue when moving a created invoice to a saved node
6
14
 
@@ -0,0 +1,93 @@
1
+ const {Transaction} = require('bitcoinjs-lib');
2
+
3
+ const inputAsOutpoint = (txId, outputIndex) => `${txId}:${outputIndex}`;
4
+ const {fromHex} = Transaction;
5
+ const sumOf = arr => arr.reduce((sum, n) => sum + n, Number());
6
+ const txIdFromHash = hash => hash.slice().reverse().toString('hex');
7
+ const uniq = arr => Array.from(new Set(arr));
8
+
9
+ /** Derive conflicted on-chain pending balances where funds are double spent or
10
+ multiple versions of the spend exist
11
+
12
+ {
13
+ transactions: [{
14
+ is_confirmed: <Transaction is Confirmed Bool>
15
+ transaction: <Raw Transaction Hex String>
16
+ }]
17
+ utxos: [{
18
+ confirmation_count: <UTXO Confirmation Count Number>
19
+ tokens: <UTXO Tokens Number>
20
+ transaction_id: <Outpoint Transaction Id Hex String>
21
+ }]
22
+ }
23
+
24
+ @returns
25
+ {
26
+ conflicting_pending_balance: <Conflicting Pending Balance Tokens Number>
27
+ invalid_pending_balance: <Invalid Pending Balance Tokens Number>
28
+ }
29
+ */
30
+ module.exports = ({transactions, utxos}) => {
31
+ const conflictingUtxos = [];
32
+ const invalidUtxos = [];
33
+ const spends = {}
34
+
35
+ // Look at unconfirmed UTXOs and collect spends of outpoints
36
+ utxos.filter(n => !n.confirmation_count).forEach((utxo, i) => {
37
+ const tx = transactions.find(n => n.id === utxo.transaction_id);
38
+
39
+ // Exit early when the raw transaction is not known
40
+ if (!tx || !tx.transaction) {
41
+ return;
42
+ }
43
+
44
+ // Register all the inputs into the spends map
45
+ return fromHex(tx.transaction).ins.forEach(input => {
46
+ const outpoint = inputAsOutpoint(txIdFromHash(input.hash), input.index);
47
+
48
+ const existing = spends[outpoint];
49
+
50
+ // When existing UTXO spends the same input this is a conflict
51
+ if (!!existing) {
52
+ conflictingUtxos.push(i);
53
+ }
54
+
55
+ // Collect spends of the outpoint
56
+ const spending = !existing ? [i] : [].concat(existing).concat(i);
57
+
58
+ return spends[outpoint] = spending;
59
+ });
60
+ });
61
+
62
+ // Look at confirmed txs and see if any unspents spend a confirmed outpoint
63
+ transactions.forEach(tx => {
64
+ // Exit early when there is no confirmed tx
65
+ if (!tx.transaction || !tx.is_confirmed) {
66
+ return;
67
+ }
68
+
69
+ // Look for pending inputs that are conflicted with a confirmed tx
70
+ return fromHex(tx.transaction).ins.forEach(input => {
71
+ const outpoint = inputAsOutpoint(txIdFromHash(input.hash), input.index);
72
+
73
+ // Exit early when nothing pending spends this outpoint
74
+ if (!spends[outpoint]) {
75
+ return;
76
+ }
77
+
78
+ // Pending things that spend a confirmed input are invalid
79
+ return spends[outpoint].forEach(n => invalidUtxos.push(n));
80
+ });
81
+ });
82
+
83
+ const conflictingTokens = uniq(conflictingUtxos)
84
+ .filter(utxoIndex => !invalidUtxos.includes(utxoIndex))
85
+ .map(n => utxos[n].tokens);
86
+
87
+ const invalidTokens = uniq(invalidUtxos).map(n => utxos[n].tokens);
88
+
89
+ return {
90
+ conflicting_pending_balance: sumOf(conflictingTokens),
91
+ invalid_pending_balance: sumOf(invalidTokens),
92
+ };
93
+ };
@@ -1,3 +1,5 @@
1
+ const conflictingBalances = require('./conflicting_balances');
2
+
1
3
  const {ceil} = Math;
2
4
  const flatten = arr => [].concat(...arr);
3
5
  const inputsCounterVBytesLength = 3;
@@ -61,6 +63,8 @@ const witnessSizeCounterVByteLength = 0.25;
61
63
  @returns
62
64
  {
63
65
  closing_balance: <Balance of Tokens Moving Out Of Channels Tokens Number>
66
+ conflicted_pending: <Conflicting Pending Balance Tokens Number>
67
+ invalid_pending: <Invalid Pending Balance Tokens Number>
64
68
  offchain_balance: <Balance of Owned Tokens In Channels Tokens Number>
65
69
  offchain_pending: <Total Pending Local Balance Tokens Number>
66
70
  onchain_balance: <Balance of Transaction Outputs Number>
@@ -170,8 +174,12 @@ module.exports = ({channels, locked, pending, transactions, utxos}) => {
170
174
  .concat(transactionLockTimeVBytesLength)
171
175
  .concat(inputElements));
172
176
 
177
+ const conflicts = conflictingBalances({transactions, utxos});
178
+
173
179
  return {
174
180
  closing_balance: sumOf(closing),
181
+ conflicted_pending: conflicts.conflicting_pending_balance,
182
+ invalid_pending: conflicts.invalid_pending_balance,
175
183
  offchain_balance: channelBalance,
176
184
  offchain_pending: pendingBalance,
177
185
  onchain_balance: chainBalance,
@@ -15,6 +15,7 @@ const {fromHex} = Transaction;
15
15
  /** Get a detailed balance that categorizes balance of tokens on the node
16
16
 
17
17
  {
18
+ [is_confirmed]: <Only Consider Confirmed Transactions Bool>
18
19
  lnd: <Authenticated LND API Object>
19
20
  }
20
21
 
@@ -113,12 +114,26 @@ module.exports = (args, cbk) => {
113
114
  locked,
114
115
  channels: getChannels.channels,
115
116
  pending: getPending.pending_channels,
116
- transactions: getTx.transactions,
117
- utxos: getUtxos.utxos,
117
+ transactions: getTx.transactions.filter(tx => {
118
+ if (!!args.is_confirmed && !tx.is_confirmed) {
119
+ return false;
120
+ }
121
+
122
+ return true;
123
+ }),
124
+ utxos: getUtxos.utxos.filter(utxo => utxo => {
125
+ if (!!args.is_confirmed && !utxo.confirmation_count) {
126
+ return false;
127
+ }
128
+
129
+ return true;
130
+ }),
118
131
  });
119
132
 
120
133
  return cbk(null, {
121
134
  closing_balance: format(balances.closing_balance) || undefined,
135
+ conflicted_pending: format(balances.conflicted_pending) || undefined,
136
+ invalid_pending: format(balances.invalid_pending) || undefined,
122
137
  offchain_balance: format(balances.offchain_balance) || undefined,
123
138
  offchain_pending: format(balances.offchain_pending) || undefined,
124
139
  onchain_balance: format(balances.onchain_balance) || undefined,
package/bos CHANGED
@@ -17,7 +17,7 @@ const inquirer = importLazy('inquirer');
17
17
  const lnService = importLazy('ln-service');
18
18
  const lnSync = importLazy('ln-sync');
19
19
  const paidServices = importLazy('paid-services');
20
- const prog = require('caporal');
20
+ const prog = require('@alexbosworth/caporal');
21
21
 
22
22
  const commandConstants = require('./commands/constants');
23
23
 
@@ -167,11 +167,7 @@ prog
167
167
  if (!!options.detailed) {
168
168
  return balances.getDetailedBalance({
169
169
  lnd,
170
- above: options.above,
171
- below: options.below,
172
170
  is_confirmed: options.confirmed,
173
- is_offchain_only: options.offchain,
174
- is_onchain_only: options.onchain,
175
171
  },
176
172
  responses.returnObject({logger, reject, resolve}));
177
173
  }
@@ -587,6 +583,7 @@ prog
587
583
  .help('You can use INBOUND_FEE_RATE to mirror an inbound fee')
588
584
  .help('You can use FEE_RATE_OF_<PUBKEY> to reference other node rates')
589
585
  .option('--node <node_name>', 'Saved node (not peer to set fees on)')
586
+ .option('--set-cltv-delta <count>', 'Set the number of blocks for CLTV', INT)
590
587
  .option('--set-fee-rate <rate>', 'Fee in parts per million or use a formula')
591
588
  .option('--to <peer>', 'Peer public key or alias to set fees', REPEATABLE)
592
589
  .action((args, options, logger) => {
@@ -594,6 +591,7 @@ prog
594
591
  try {
595
592
  return routing.adjustFees({
596
593
  logger,
594
+ cltv_delta: options.setCltvDelta,
597
595
  fee_rate: options.setFeeRate,
598
596
  fs: {getFile: readFile},
599
597
  lnd: (await lnd.authenticatedLnd({logger, node: options.node})).lnd,
@@ -1627,6 +1625,7 @@ prog
1627
1625
  logger,
1628
1626
  ask: (n, cbk) => inquirer.prompt([n]).then(res => cbk(null, res)),
1629
1627
  lnd: (await lndForNode(logger, options.node)).lnd,
1628
+ separator: () => new inquirer.Separator(),
1630
1629
  },
1631
1630
  responses.returnObject({exit, logger, reject, resolve}));
1632
1631
  } catch (err) {
package/package.json CHANGED
@@ -10,19 +10,19 @@
10
10
  "url": "https://github.com/alexbosworth/balanceofsatoshis/issues"
11
11
  },
12
12
  "dependencies": {
13
- "@alexbosworth/fiat": "1.0.0",
13
+ "@alexbosworth/caporal": "1.4.0",
14
+ "@alexbosworth/fiat": "1.0.1",
14
15
  "@alexbosworth/html2unicode": "1.1.5",
15
16
  "@alexbosworth/node-fetch": "2.6.2",
16
17
  "abort-controller": "3.0.0",
17
18
  "asciichart": "1.5.25",
18
- "async": "3.2.2",
19
- "asyncjs-util": "1.2.7",
19
+ "async": "3.2.3",
20
+ "asyncjs-util": "1.2.8",
20
21
  "bip66": "1.1.5",
21
22
  "bitcoinjs-lib": "6.0.1",
22
23
  "bolt01": "1.2.3",
23
- "bolt03": "1.2.12",
24
+ "bolt03": "1.2.13",
24
25
  "bolt07": "1.8.0",
25
- "caporal": "1.4.0",
26
26
  "cbor": "8.1.0",
27
27
  "colorette": "2.0.16",
28
28
  "crypto-js": "4.1.1",
@@ -33,14 +33,14 @@
33
33
  "import-lazy": "4.0.0",
34
34
  "ini": "2.0.0",
35
35
  "inquirer": "8.2.0",
36
- "invoices": "2.0.2",
36
+ "invoices": "2.0.3",
37
37
  "ln-accounting": "5.0.5",
38
- "ln-service": "53.2.0",
38
+ "ln-service": "53.4.0",
39
39
  "ln-sync": "3.6.1",
40
40
  "ln-telegram": "3.5.1",
41
41
  "moment": "2.29.1",
42
42
  "paid-services": "3.5.1",
43
- "probing": "2.0.1",
43
+ "probing": "2.0.2",
44
44
  "psbt": "1.1.10",
45
45
  "qrcode-terminal": "0.12.0",
46
46
  "sanitize-filename": "1.6.3",
@@ -51,7 +51,7 @@
51
51
  "description": "Lightning balance CLI",
52
52
  "devDependencies": {
53
53
  "@alexbosworth/tap": "15.0.10",
54
- "ln-docker-daemons": "2.1.0",
54
+ "ln-docker-daemons": "2.2.0",
55
55
  "mock-lnd": "1.4.1",
56
56
  "secp256k1": "4.0.3"
57
57
  },
@@ -69,8 +69,15 @@
69
69
  "license": "MIT",
70
70
  "main": "index.js",
71
71
  "name": "balanceofsatoshis",
72
- "resolutions": {
73
- "colors": "1.1.2"
72
+ "overrides": {
73
+ "caporal@1.4.0": {
74
+ "cli-table3@0.5.1": {
75
+ "colors@1.4.2": "1.4.0"
76
+ },
77
+ "prettyjson@1.2.1": {
78
+ "colors@1.4.2": "1.4.0"
79
+ }
80
+ }
74
81
  },
75
82
  "repository": {
76
83
  "type": "git",
@@ -81,8 +88,7 @@
81
88
  "integration-tests": "tap --branches=1 --functions=1 --lines=1 --statements=1 -t 120 test/integration/*.js",
82
89
  "postpack": "PACKAGE_VERSION=$(cat package.json | grep \\\"version\\\" | head -1 | awk -F: '{ print $2 }' | sed 's/[\",]//g' | tr -d '[[:space:]]') && git tag -s v$PACKAGE_VERSION -m v$PACKAGE_VERSION && git push github --tags",
83
90
  "postpublish": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t alexbosworth/balanceofsatoshis --push .",
84
- "preinstall": "npx npm-force-resolutions",
85
91
  "test": "tap --branches=1 --functions=1 --lines=1 --statements=1 -t 60 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/wallets/*.js"
86
92
  },
87
- "version": "11.22.2"
93
+ "version": "11.24.0"
88
94
  }
@@ -13,13 +13,13 @@ const {getPendingChannels} = require('ln-service');
13
13
  const {gray} = require('colorette');
14
14
  const {green} = require('colorette');
15
15
  const moment = require('moment');
16
- const {Parser} = require('hot-formula-parser');
17
16
  const {returnResult} = require('asyncjs-util');
18
17
  const {updateChannelFee} = require('ln-sync');
19
18
 
20
19
  const {chartAliasForPeer} = require('./../display');
21
20
  const {formatFeeRate} = require('./../display');
22
21
  const {getIcons} = require('./../display');
22
+ const parseFeeRateFormula = require('./parse_fee_rate_formula');
23
23
 
24
24
  const asTxOut = n => `${n.transaction_id}:${n.transaction_vout}`;
25
25
  const {ceil} = Math;
@@ -27,16 +27,19 @@ const flatten = arr => [].concat(...arr);
27
27
  const interval = 1000 * 60 * 2;
28
28
  const {isArray} = Array;
29
29
  const {max} = Math;
30
+ const minCltvDelta = 18;
30
31
  const nodeMatch = /\bFEE_RATE_OF_[0-9A-F]{66}\b/gim;
31
32
  const noFee = gray('Unknown Rate');
32
33
  const pubKeyForNodeMatch = n => n.substring(12).toLowerCase();
33
34
  const shortKey = key => key.substring(0, 20);
35
+ const sumOf = arr => arr.reduce((sum, n) => sum + n, 0);
34
36
  const times = 360;
35
37
  const uniq = arr => Array.from(new Set(arr));
36
38
 
37
39
  /** View and adjust routing fees
38
40
 
39
41
  {
42
+ [cltv_delta]: <Set CLTV Delta Number>
40
43
  [fee_rate]: <Fee Rate String>
41
44
  fs: {
42
45
  getFile: <Read File Contents Function> (path, cbk) => {}
@@ -72,6 +75,14 @@ module.exports = (args, cbk) => {
72
75
  return cbk([400, 'ExpectedArrayOfPeersToAdjustFeesTowards']);
73
76
  }
74
77
 
78
+ if (args.cltv_delta !== undefined && !args.to.length) {
79
+ return cbk([400, 'SettingGlobalCltvDeltaNotSupported']);
80
+ }
81
+
82
+ if (args.cltv_delta !== undefined && args.cltv_delta < minCltvDelta) {
83
+ return cbk([400, 'SettingLowCltvDeltaIsNotSupported']);
84
+ }
85
+
75
86
  if (args.fee_rate !== undefined && !args.to.length) {
76
87
  return cbk([400, 'SettingGlobalFeeRateNotSupported']);
77
88
  }
@@ -227,7 +238,8 @@ module.exports = (args, cbk) => {
227
238
  },
228
239
  cbk) =>
229
240
  {
230
- if (args.fee_rate === undefined) {
241
+ // Exit early when not updating policy
242
+ if (args.cltv_delta === undefined && args.fee_rate === undefined) {
231
243
  return cbk();
232
244
  }
233
245
 
@@ -240,16 +252,6 @@ module.exports = (args, cbk) => {
240
252
  .concat(getPending.pending_channels.filter(n => !!n.is_opening))
241
253
  .filter(channel => channel.partner_public_key === key);
242
254
 
243
- const inboundLiquidity = channels.reduce((sum, n) => {
244
- return sum + n.remote_balance;
245
- },
246
- Number());
247
-
248
- const outboundLiquidity = channels.reduce((sum, n) => {
249
- return sum + n.local_balance;
250
- },
251
- Number());
252
-
253
255
  const peerPolicies = getPolicies
254
256
  .filter(n => !!n)
255
257
  .filter(n => channels.find(chan => asTxOut(chan) === asTxOut(n)))
@@ -258,51 +260,6 @@ module.exports = (args, cbk) => {
258
260
 
259
261
  const inboundFeeRate = max(...peerPolicies.map(n => n.fee_rate));
260
262
 
261
- const parser = new Parser();
262
-
263
- parser.setVariable('INBOUND', inboundLiquidity);
264
- parser.setVariable('INBOUND_FEE_RATE', inboundFeeRate);
265
- parser.setVariable('OUTBOUND', outboundLiquidity);
266
-
267
- getNodeRates.forEach(({key, rate}) => parser.setVariable(key, rate));
268
-
269
- parser.setFunction('BIPS', params => {
270
- const [param] = params;
271
-
272
- return params * 1e2;
273
- });
274
-
275
- parser.setFunction('PERCENT', params => {
276
- const [param] = params;
277
-
278
- return params * 1e4;
279
- });
280
-
281
- const parsedRate = parser.parse(args.fee_rate.toUpperCase());
282
-
283
- switch (parsedRate.error) {
284
- case '#DIV/0!':
285
- return cbk([503, 'FeeRateCalculationCannotDivideByZeroFormula']);
286
-
287
- case '#ERROR!':
288
- return cbk([503, 'FailedToParseFeeRateFormula']);
289
-
290
- case '#N/A':
291
- case '#NAME?':
292
- return cbk([503, 'UnrecognizedVariableOrFunctionInFeeRateFormula']);
293
-
294
- case '#NUM':
295
- return cbk([503, 'InvalidNumberFoundInFeeRateFormula']);
296
-
297
- case '#VALUE!':
298
- return cbk([503, 'UnexpectedValueTypeInFeeRateFormula']);
299
-
300
- default:
301
- break;
302
- }
303
-
304
- const feeRate = ceil(parsedRate.result);
305
-
306
263
  const feeRates = getFeeRates.channels.filter(rate => {
307
264
  return channels.find(n => asTxOut(n) === asTxOut(rate));
308
265
  });
@@ -317,20 +274,39 @@ module.exports = (args, cbk) => {
317
274
  .map(n => BigInt(n.base_fee_mtokens))
318
275
  .reduce((sum, fee) => fee > sum ? fee : sum, BigInt(Number()));
319
276
 
277
+ const {failure, rate} = parseFeeRateFormula({
278
+ fee_rate: args.fee_rate,
279
+ inbound_fee_rate: inboundFeeRate,
280
+ inbound_liquidity: sumOf(channels.map(n => n.remote_balance)),
281
+ outbound_liquidity: sumOf(channels.map(n => n.local_balance)),
282
+ node_rates: getNodeRates,
283
+ });
284
+
285
+ if (!!failure) {
286
+ return cbk([400, failure]);
287
+ }
288
+
320
289
  return cbk(null, channels.map(channel => {
321
290
  // Exit early when there is no known policy
322
291
  if (!currentPolicies.length) {
323
292
  return {
324
- fee_rate: feeRate,
293
+ cltv_delta: cltvDelta,
294
+ fee_rate: rate,
325
295
  transaction_id: channel.transaction_id,
326
296
  transaction_vout: channel.transaction_vout,
327
297
  };
328
298
  }
329
299
 
300
+ // Only the highest CLTV delta across all peer channels applies
301
+ const cltvDelta = max(...currentPolicies.map(n => n.cltv_delta));
302
+
303
+ // Only the highest fee rate across all peer channels applies
304
+ const maxFeeRate = max(...currentPolicies.map(n => n.fee_rate));
305
+
330
306
  return {
331
307
  base_fee_mtokens: baseFeeMillitokens.toString(),
332
- cltv_delta: max(...currentPolicies.map(n => n.cltv_delta)),
333
- fee_rate: feeRate,
308
+ cltv_delta: args.cltv_delta || cltvDelta,
309
+ fee_rate: rate !== undefined ? rate : maxFeeRate,
334
310
  transaction_id: channel.transaction_id,
335
311
  transaction_vout: channel.transaction_vout,
336
312
  };
@@ -0,0 +1,58 @@
1
+ const {Parser} = require('hot-formula-parser');
2
+
3
+ const bipsAsPpm = bips => bips * 1e2;
4
+ const {ceil} = Math;
5
+ const percentAsPpm = percent => percent * 1e4;
6
+
7
+ /** Parse a fee rate formula
8
+
9
+ {
10
+ [fee_rate]: <PPM Fee Rate String>
11
+ inbound_fee_rate: <Inbound PPM Fee Rate Number>
12
+ inbound_liquidity: <Inbound Tokens Number>
13
+ outbound_liquidity: <Outbound Tokens Number>
14
+ node_rates: [{
15
+ key: <Node Key String>
16
+ rate: <Node PPM Rate Number>
17
+ }]
18
+ }
19
+
20
+ @returns
21
+ {
22
+ [failure]: <Failure to Parse String>
23
+ [rate]: <PPM Fee Rate Number>
24
+ }
25
+ */
26
+ module.exports = args => {
27
+ if (args.fee_rate === undefined) {
28
+ return {};
29
+ }
30
+
31
+ const parser = new Parser();
32
+
33
+ parser.setFunction('BIPS', params => bipsAsPpm(params.slice().pop()));
34
+ parser.setFunction('PERCENT', params => percentAsPpm(params.slice().pop()));
35
+ parser.setVariable('INBOUND', args.inbound_liquidity);
36
+ parser.setVariable('INBOUND_FEE_RATE', args.inbound_fee_rate);
37
+ parser.setVariable('OUTBOUND', args.outbound_liquidity);
38
+
39
+ args.node_rates.forEach(({key, rate}) => parser.setVariable(key, rate));
40
+
41
+ const parsedRate = parser.parse(args.fee_rate.toUpperCase());
42
+
43
+ switch (parsedRate.error) {
44
+ case null:
45
+ break;
46
+
47
+ case '#DIV/0!':
48
+ return {failure: 'FeeRateCalculationCannotDivideByZeroFormula'};
49
+
50
+ case '#ERROR!':
51
+ return {failure: 'FailedToParseFeeRateFormula'};
52
+
53
+ default:
54
+ return {failure: 'UnrecognizedVariableOrFunctionInFeeRateFormula'};
55
+ }
56
+
57
+ return {rate: ceil(parsedRate.result)};
58
+ };
@@ -93,6 +93,8 @@ const tests = [
93
93
  description: 'Balance totals are calculated',
94
94
  expected: {
95
95
  closing_balance: 1,
96
+ conflicted_pending: 0,
97
+ invalid_pending: 0,
96
98
  offchain_balance: 14,
97
99
  offchain_pending: 35,
98
100
  onchain_balance: 4,
@@ -104,6 +106,8 @@ const tests = [
104
106
  description: 'Balance totals are calculated when there are no funds',
105
107
  expected: {
106
108
  closing_balance: 0,
109
+ conflicted_pending: 0,
110
+ invalid_pending: 0,
107
111
  offchain_balance: 0,
108
112
  offchain_pending: 0,
109
113
  onchain_balance: 0,
@@ -98,6 +98,10 @@ const tests = [
98
98
  'income',
99
99
  ],
100
100
  ],
101
+ rows_summary: [
102
+ ['Total', 'Asset', 'Report Date', 'Total Fiat'],
103
+ [1, 'BTC', '', '0.00'],
104
+ ],
101
105
  },
102
106
  },
103
107
  {
@@ -114,6 +118,10 @@ tests.forEach(({args, description, error, expected}) => {
114
118
  } else {
115
119
  const res = await getAccountingReport(args);
116
120
 
121
+ if (!args.is_csv) {
122
+ res.rows_summary[1][2] = '';
123
+ }
124
+
117
125
  strictSame(res, expected, 'Got expected response');
118
126
  }
119
127
 
@@ -0,0 +1,67 @@
1
+ const {test} = require('@alexbosworth/tap');
2
+
3
+ const method = require('./../../routing/parse_fee_rate_formula');
4
+
5
+ const makeArgs = overrides => {
6
+ const args = {
7
+ fee_rate: '1',
8
+ inbound_fee_rate: 1,
9
+ inbound_liquidity: 1,
10
+ outbound_liquidity: 1,
11
+ node_rates: [{key: 'key', rate: 1}],
12
+ };
13
+
14
+ Object.keys(overrides).forEach(k => args[k] = overrides[k]);
15
+
16
+ return args;
17
+ };
18
+
19
+ const tests = [
20
+ {
21
+ args: makeArgs({fee_rate: undefined}),
22
+ description: 'Fee rate is optional',
23
+ expected: {},
24
+ },
25
+ {
26
+ args: makeArgs({}),
27
+ description: 'Fee rate formula is parsed',
28
+ expected: {rate: 1},
29
+ },
30
+ {
31
+ args: makeArgs({fee_rate: 'BIPS(25)'}),
32
+ description: 'BIPs function is parsed',
33
+ expected: {rate: 2500},
34
+ },
35
+ {
36
+ args: makeArgs({fee_rate: 'PERCENT(0.25)'}),
37
+ description: 'PERCENT function is parsed',
38
+ expected: {rate: 2500},
39
+ },
40
+ {
41
+ args: makeArgs({fee_rate: '1/0'}),
42
+ description: 'Cannot divide by zero',
43
+ expected: {failure: 'FeeRateCalculationCannotDivideByZeroFormula'},
44
+ },
45
+ {
46
+ args: makeArgs({fee_rate: '/'}),
47
+ description: 'Formula must be valid',
48
+ expected: {failure: 'FailedToParseFeeRateFormula'},
49
+ },
50
+ {
51
+ args: makeArgs({fee_rate: 'fee_rate'}),
52
+ description: 'Formula must be valid',
53
+ expected: {failure: 'UnrecognizedVariableOrFunctionInFeeRateFormula'},
54
+ },
55
+ ];
56
+
57
+ tests.forEach(({args, description, error, expected}) => {
58
+ return test(description, async ({end, strictSame, throws}) => {
59
+ if (!!error) {
60
+ throws(() => method(args), new Error(error), 'Got expected error');
61
+ } else {
62
+ strictSame(method(args), expected, 'Got expected result');
63
+ }
64
+
65
+ return end();
66
+ });
67
+ });