balanceofsatoshis 11.22.4 → 11.26.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 +19 -0
- package/balances/conflicting_balances.js +93 -0
- package/balances/detailed_balances.js +8 -0
- package/balances/get_detailed_balance.js +17 -2
- package/bos +3 -4
- package/commands/api.json +18 -0
- package/package.json +10 -10
- package/routing/adjust_fees.js +36 -60
- package/routing/parse_fee_rate_formula.js +58 -0
- package/test/balances/test_detailed_balance.js +4 -0
- package/test/balances/test_get_accounting_report.js +8 -0
- package/test/{services → open}/test_open_balanced_channel.js +0 -0
- package/test/routing/test_parse_fee_rate_formula.js +67 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Versions
|
|
2
2
|
|
|
3
|
+
## 11.26.0
|
|
4
|
+
|
|
5
|
+
- `call`: Add support for `deletePendingChannel` to remove stuck pending chans
|
|
6
|
+
|
|
7
|
+
## 11.25.0
|
|
8
|
+
|
|
9
|
+
- `trade-secret`: persist open trades, with expiration dates and longer-lived
|
|
10
|
+
trade support.
|
|
11
|
+
- `trade-secret`: show the raw encoded trade after requesting an open trade
|
|
12
|
+
- `trade-secret`: confirm RPC signer support before allowing trade start
|
|
13
|
+
|
|
14
|
+
## 11.24.0
|
|
15
|
+
|
|
16
|
+
- `fees`: Add `--set-cltv-delta` to control the forwarding CLTV delta with peer
|
|
17
|
+
|
|
18
|
+
## 11.23.0
|
|
19
|
+
|
|
20
|
+
- `balance`: `--detailed`: Support unconfirmed/invalid/conflicting balances
|
|
21
|
+
|
|
3
22
|
## 11.22.4
|
|
4
23
|
|
|
5
24
|
- `telegram`: Fix issue when moving a created invoice to a saved node
|
|
@@ -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
|
-
|
|
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
|
@@ -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/commands/api.json
CHANGED
|
@@ -164,6 +164,24 @@
|
|
|
164
164
|
{
|
|
165
165
|
"method": "deletePayments"
|
|
166
166
|
},
|
|
167
|
+
{
|
|
168
|
+
"arguments": [
|
|
169
|
+
{
|
|
170
|
+
"description": "Conflicting confirmed transaction",
|
|
171
|
+
"named": "confirmed_transaction"
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
"description": "Stuck pending transaction",
|
|
175
|
+
"named": "pending_transaction"
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
"description": "Stuck pending channel transaction output index",
|
|
179
|
+
"named": "pending_transaction_vout",
|
|
180
|
+
"type": "number"
|
|
181
|
+
}
|
|
182
|
+
],
|
|
183
|
+
"method": "deletePendingChannel"
|
|
184
|
+
},
|
|
167
185
|
{
|
|
168
186
|
"arguments": [
|
|
169
187
|
{
|
package/package.json
CHANGED
|
@@ -11,17 +11,17 @@
|
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
13
|
"@alexbosworth/caporal": "1.4.0",
|
|
14
|
-
"@alexbosworth/fiat": "1.0.
|
|
14
|
+
"@alexbosworth/fiat": "1.0.1",
|
|
15
15
|
"@alexbosworth/html2unicode": "1.1.5",
|
|
16
16
|
"@alexbosworth/node-fetch": "2.6.2",
|
|
17
17
|
"abort-controller": "3.0.0",
|
|
18
18
|
"asciichart": "1.5.25",
|
|
19
|
-
"async": "3.2.
|
|
20
|
-
"asyncjs-util": "1.2.
|
|
19
|
+
"async": "3.2.3",
|
|
20
|
+
"asyncjs-util": "1.2.8",
|
|
21
21
|
"bip66": "1.1.5",
|
|
22
22
|
"bitcoinjs-lib": "6.0.1",
|
|
23
23
|
"bolt01": "1.2.3",
|
|
24
|
-
"bolt03": "1.2.
|
|
24
|
+
"bolt03": "1.2.13",
|
|
25
25
|
"bolt07": "1.8.0",
|
|
26
26
|
"cbor": "8.1.0",
|
|
27
27
|
"colorette": "2.0.16",
|
|
@@ -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.
|
|
36
|
+
"invoices": "2.0.3",
|
|
37
37
|
"ln-accounting": "5.0.5",
|
|
38
|
-
"ln-service": "53.
|
|
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
|
-
"paid-services": "3.
|
|
43
|
-
"probing": "2.0.
|
|
42
|
+
"paid-services": "3.6.0",
|
|
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.
|
|
54
|
+
"ln-docker-daemons": "2.2.0",
|
|
55
55
|
"mock-lnd": "1.4.1",
|
|
56
56
|
"secp256k1": "4.0.3"
|
|
57
57
|
},
|
|
@@ -90,5 +90,5 @@
|
|
|
90
90
|
"postpublish": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t alexbosworth/balanceofsatoshis --push .",
|
|
91
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"
|
|
92
92
|
},
|
|
93
|
-
"version": "11.
|
|
93
|
+
"version": "11.26.0"
|
|
94
94
|
}
|
package/routing/adjust_fees.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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:
|
|
333
|
-
fee_rate:
|
|
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
|
|
|
File without changes
|
|
@@ -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
|
+
});
|