balanceofsatoshis 11.0.0 → 11.3.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,26 @@
1
1
  # Versions
2
2
 
3
+ ## Version 11.3.0
4
+
5
+ - `credentials`: Allow specifying specific methods to allow in a credential
6
+
7
+ ## Version 11.2.1
8
+
9
+ - Improve support for LND v0.13.3
10
+
11
+ ## Version 11.2.0
12
+
13
+ - `rebalance`: Add support for `key/formula` expressions in `--avoid`
14
+
15
+ ## Version 11.1.0
16
+
17
+ - `avoid`: Add `capacity` to reference channel capacity amount
18
+ - `call`: Add support for force closing a channel
19
+ - `find`: Improve lookup speed when querying a tx id or peer
20
+ - `open-balanced-channel`: Avoid showing incoming requests that were accepted
21
+ - `rebalance`: Add `capacity` variable to `--in-filter` and `--out-filter`
22
+ - `transfer`: Add `in_inbound` and `in_outbound` variables to amount formulas
23
+
3
24
  ## Version 11.0.0
4
25
 
5
26
  - `rebalance`: Add `--in-filter` to filter inbound tagged nodes
package/README.md CHANGED
@@ -524,3 +524,134 @@ You can also create an alias to run a command in the background
524
524
  ```shell
525
525
  alias bosd="docker run -d --rm -v $HOME/.bos:/home/node/.bos alexbosworth/balanceofsatoshis"
526
526
  ```
527
+
528
+ ## Formulas
529
+
530
+ Some commands take formula arguments. Formulas are expressions that allow you to perform
531
+ functions and reference variables.
532
+
533
+ There is a dynamic playground here where you can play with expressions:
534
+ https://formulajs.info/functions/
535
+
536
+ ### `amount`
537
+
538
+ Formula amounts are supported in the following commands:
539
+
540
+ - `fund`
541
+ - `inbound-channel-rules`
542
+ - `open`
543
+ - `probe`
544
+ - `rebalance`
545
+ - `send`
546
+
547
+ When passing an amount you can pass a formula expression, and the following variables are
548
+ defined:
549
+
550
+ - `k`: 1,000
551
+ - `m`: 1,000,000
552
+
553
+ Examples:
554
+
555
+ ```
556
+ bos fund <address> 7*m
557
+ // Fund address with value 7,000,000
558
+
559
+ bos probe <key> 100*k
560
+ // Probe to key amount 100,000
561
+
562
+ bos send <key> m/2
563
+ // Push 500,000 to key
564
+ ```
565
+
566
+ #### `rebalance`
567
+
568
+ Rebalance defines additional variables for `--amount`:
569
+
570
+ - `capacity`: The total of inbound and outbound
571
+
572
+ And for `--in-filter` and `--out-filter`:
573
+
574
+ - `capacity`: The total capacity with the peer
575
+ - `heights`: The set of heights of the channels with the peer
576
+ - `inbound_liquidity`: The inbound liquidity with the peer
577
+ - `outbound_liquidity`: The outbound liquidity with the peer
578
+
579
+ Example:
580
+
581
+ ```
582
+ // Rebalance with a target of 1,000,000
583
+ bos rebalance --amount 1*m
584
+ ```
585
+
586
+ #### `send`
587
+
588
+ Send defines additional variables:
589
+
590
+ - `eur`: The value of 1 Euro as defined by rate provider
591
+ - `inbound`: The inbound liquidity with the destination
592
+ - `liquidity`: The total capacity with the destination
593
+ - `outbound`: The inbound liquidity with the destination
594
+ - `usd`: The value of 1 US Dollar as defined by rate provider
595
+
596
+ Example:
597
+
598
+ ```
599
+ // Send node $1
600
+ bos send <key> --amount 1*usd
601
+ ```
602
+
603
+ #### `transfer`
604
+
605
+ Transfer variables:
606
+
607
+ - `out_inbound`: The outbound liquidity with the outbound peer
608
+ - `out_liquidity`: The total inbound+outbound with the outbound peer
609
+ - `out_outbound`: The total outbound liquidity with the outbound peer
610
+
611
+ Example:
612
+
613
+ ```
614
+ // Equalize inbound with a mutual peer
615
+ bos transfer node "in_inbound - (in_inbound + out_inbound)/2" --through peer
616
+ ```
617
+
618
+ ### `fees`
619
+
620
+ Variables can be referenced for `--set-fee-rate`
621
+
622
+ - `fee_rate_of_<pubkey>`: Reference other node's fee rate
623
+ - `inbound`: Remote balance with peer
624
+ - `inbound_fee_rate`: Incoming fee rate
625
+ - `outbound`: Local balance with peer
626
+
627
+ You can also use functions:
628
+
629
+ - `bips(n)`: Set fee as parts per thousand
630
+ - `percent(0.00)`: Set fee as fractional percentage
631
+
632
+ Example:
633
+
634
+ ```
635
+ // Set the fee rate to a tag to 1% of the value forwarded
636
+ bos fees --to tag --set-fee-rate "percent(1)"
637
+ ```
638
+
639
+ ### `inbound-channel-rules`
640
+
641
+ Pass formulas for rules with `--rule`.
642
+
643
+ Formula variables:
644
+
645
+ - `capacities`: sizes of the peer's public channels
646
+ - `capacity`: size of the inbound channel
647
+ - `channel_ages`: block ages of the peer's public channels
648
+ - `fee_rates`: outbound fee rates for the peer
649
+ - `local_balance`: gifted amount on the incoming channel
650
+ - `public_key`: key of the incoming peer
651
+
652
+ Example:
653
+
654
+ ```
655
+ // Reject channels that are smaller than 2,000,000 capacity
656
+ bos inbound-channel-rules "capacity < 2*m"
657
+ ```
package/bos CHANGED
@@ -420,6 +420,7 @@ prog
420
420
  .help('Output encrypted remote access credentials. Use with "nodes --add"')
421
421
  .option('--cleartext', 'Output remote access credentials without encryption')
422
422
  .option('--days <days>', 'Expiration days for credentials', INT, 365)
423
+ .option('--method <method_name>', 'White-list specific method', REPEATABLE)
423
424
  .option('--node <node_name>', 'Get credentials for a saved node')
424
425
  .option('--nospend', 'Credentials do not include spending privileges')
425
426
  .option('--readonly', 'Credentials only include read permissions')
@@ -432,6 +433,7 @@ prog
432
433
  is_cleartext: options.cleartext,
433
434
  is_nospend: options.nospend,
434
435
  is_readonly: options.readonly,
436
+ methods: flatten([options.method].filter(n => !!n)),
435
437
  node: options.node,
436
438
  },
437
439
  responses.returnObject({logger, reject, resolve}));
@@ -1174,18 +1176,19 @@ prog
1174
1176
  .help('--avoid can take a channel id or a public key to avoid')
1175
1177
  .help('--avoid can take a public_key/public_key to avoid a directed pair')
1176
1178
  .help('--avoid can take a FORMULA/public_key to avoid inbound peers')
1179
+ .help('--avoid can take a public_key/FORMULA to avoid outbound peers')
1177
1180
  .help('--avoid FORMULA variables: FEE_RATE, BASE_FEE, HEIGHT, AGE')
1178
1181
  .help('--in decreases the inbound liquidity with a specific peer/tag')
1179
- .help('--in-filter vars: HEIGHTS, INBOUND_LIQUIDITY/OUTBOUND_LIQUIDITY')
1182
+ .help('--in-filter vars: CAPACITY/HEIGHTS/(INBOUND|OUTBOUND)_LIQUIDITY')
1180
1183
  .help('--out increases the inbound liquidity with a specific peer/tag')
1181
- .help('--out-filter vars: HEIGHTS, INBOUND_LIQUIDITY/OUTBOUND_LIQUIDITY')
1184
+ .help('--out-filter vars: CAPACITY/HEIGHTS(INBOUND|OUTBOUND)_LIQUIDITY')
1182
1185
  .option('--amount <amount>', 'Maximum amount to rebalance')
1183
1186
  .option('--avoid <pubkey_or_chanid>', 'Avoid forwarding through', REPEATABLE)
1184
1187
  .option('--in <pubkey_or_alias>', 'Route in through a specific peer')
1185
1188
  .option('--in-filter <in_filter>', 'Filter inbound tag nodes', REPEATABLE)
1186
1189
  .option('--in-target-outbound <amt>', 'Balance up to outbound amount')
1187
- .option('--max-fee <max_fee>', 'Maximum fee to pay', INT)
1188
- .option('--max-fee-rate <max_fee_rate>', 'Max fee rate to pay', INT)
1190
+ .option('--max-fee <max_fee>', 'Maximum fee to pay')
1191
+ .option('--max-fee-rate <max_fee_rate>', 'Max fee rate to pay')
1189
1192
  .option('--minutes <minutes>', 'Time-out route search after N minutes', INT)
1190
1193
  .option('--no-color', 'Mute all colors')
1191
1194
  .option('--node <node_name>', 'Node to use for rebalance')
@@ -1560,6 +1563,7 @@ prog
1560
1563
  .help('Formulas are supported in amount')
1561
1564
  .help('Also supported in formulas: OUT_LIQUIDITY (with outbound peer)')
1562
1565
  .help('OUT_INBOUND, OUT_OUTBOUND (when specifying outbound peer)')
1566
+ .help('IN_INBOUND, IN_OUTBOUND (when specifying inbound peer)')
1563
1567
  .option('--description', 'Label describing transfer')
1564
1568
  .option('--dryrun', 'Avoid actually sending funds')
1565
1569
  .option('--in <pubkey_or_alias>', 'Route in through a specific peer')
package/commands/api.json CHANGED
@@ -50,11 +50,22 @@
50
50
  },
51
51
  {
52
52
  "arguments": [
53
+ {
54
+ "description": "Request close out to address for cooperative close",
55
+ "named": "address",
56
+ "optional": true
57
+ },
53
58
  {
54
59
  "description": "Requested chain fee tokens per vbyte",
55
60
  "named": "tokens_per_vbyte",
61
+ "optional": true,
56
62
  "type": "number"
57
63
  },
64
+ {
65
+ "description": "Force close the channel",
66
+ "named": "is_force_close",
67
+ "type": "boolean"
68
+ },
58
69
  {
59
70
  "description": "Hex encoded funding transaction id",
60
71
  "named": "transaction_id",
@@ -77,6 +77,16 @@ module.exports = ({ask, lnd, logger, method}, cbk) => {
77
77
  }
78
78
 
79
79
  return asyncMapSeries(arguments, (argument, cbk) => {
80
+ if (argument.type === 'boolean') {
81
+ return ask({
82
+ default: false,
83
+ name: argument.named,
84
+ message: argument.description,
85
+ type: 'confirm',
86
+ },
87
+ cbk);
88
+ }
89
+
80
90
  return ask({
81
91
  default: () => !!argument.optional ? String() : undefined,
82
92
  message: argument.description,
package/fiat/index.js CHANGED
@@ -3,7 +3,6 @@ const getCoindeskCurrentPrice = require('./get_coindesk_current_price');
3
3
  const getCoindeskRates = require('./get_coindesk_rates');
4
4
  const getCoingeckoRates = require('./get_coingecko_rates');
5
5
  const getExchangeRates = require('./get_exchange_rates');
6
- const getPriceChart = require('./get_price_chart');
7
6
  const getPrices = require('./get_prices');
8
7
  const marketPairs = require('./market').pairs;
9
8
  const priceProviders = require('./market').price_providers;
@@ -22,7 +21,6 @@ module.exports = {
22
21
  getCoindeskRates,
23
22
  getCoingeckoRates,
24
23
  getExchangeRates,
25
- getPriceChart,
26
24
  getPrices,
27
25
  pairs,
28
26
  priceProviders,
@@ -0,0 +1,41 @@
1
+ const {noSpendPerms} = require('./constants');
2
+ const {permissionEntities} = require('./constants');
3
+
4
+ const readPerms = permissionEntities.map(entity => `${entity}:read`);
5
+
6
+ /** Derive restrictions for macaroon
7
+
8
+ {
9
+ [is_nospend]: <Restrict Credentials To Non-Spending Permissions Bool>
10
+ [is_readonly]: <Restrict Credentials To Read-Only Permissions Bool>
11
+ [methods]: [<Allow Specific Method String>]
12
+ }
13
+
14
+ @returns
15
+ {
16
+ [allow]: {
17
+ methods: [<Allow Specific Method String>]
18
+ permissions: [<Entity:Action String>]
19
+ }
20
+ }
21
+ */
22
+ module.exports = args => {
23
+ const methods = args.methods || [];
24
+
25
+ // Exit early when specific credentials are not requested
26
+ if (!args.is_readonly && !args.is_nospend && !methods.length) {
27
+ return {};
28
+ }
29
+
30
+ const permissions = [];
31
+
32
+ if (!!args.is_readonly) {
33
+ readPerms.forEach(n => permissions.push(n));
34
+ }
35
+
36
+ if (!!args.is_nospend) {
37
+ noSpendPerms.forEach(n => permissions.push(n));
38
+ }
39
+
40
+ return {allow: {methods, permissions}};
41
+ };
@@ -1,4 +1,5 @@
1
1
  const asyncAuto = require('async/auto');
2
+ const asyncReflect = require('async/reflect');
2
3
  const {chanFormat} = require('bolt07');
3
4
  const {formatTokens} = require('ln-sync');
4
5
  const {getChannel} = require('ln-service');
@@ -6,17 +7,21 @@ const {getChannels} = require('ln-service');
6
7
  const {getClosedChannels} = require('ln-service');
7
8
  const {getHeight} = require('ln-service');
8
9
  const {getNetworkGraph} = require('ln-service');
10
+ const {getNode} = require('ln-service');
9
11
  const {getPayment} = require('ln-service');
10
12
  const {getTransactionRecord} = require('ln-sync');
11
13
  const {gray} = require('colorette');
12
14
  const moment = require('moment');
13
15
  const {returnResult} = require('asyncjs-util');
14
16
 
17
+ const {findKey} = require('ln-sync');
18
+
15
19
  const asBigUnit = tokens => (tokens / 1e8).toFixed(8);
16
20
  const balance = ({display}) => display.trim() || gray('0.00000000');
17
21
  const blocksTime = (n, p) => moment.duration(n * 10, 'minutes').humanize(p);
18
22
  const {isArray} = Array;
19
23
  const isHash = n => !!n && /^[0-9A-F]{64}$/i.test(n);
24
+ const isPublicKey = n => !!n && /^0[2-3][0-9A-F]{64}$/i.test(n);
20
25
  const notFound = 404;
21
26
  const standardIdHexLength = Buffer.alloc(32).toString('hex').length;
22
27
 
@@ -77,8 +82,47 @@ module.exports = ({lnd, query}, cbk) => {
77
82
  // Get closed
78
83
  getClosed: ['validate', ({}, cbk) => getClosedChannels({lnd}, cbk)],
79
84
 
85
+ // Determine the public key to use
86
+ getKey: ['validate', asyncReflect(({}, cbk) => {
87
+ if (query.length === standardIdHexLength) {
88
+ return cbk();
89
+ }
90
+
91
+ return findKey({lnd, query}, cbk);
92
+ })],
93
+
80
94
  // Get graph
81
- getGraph: ['validate', ({}, cbk) => getNetworkGraph({lnd}, cbk)],
95
+ getGraph: ['getKey', ({getKey}, cbk) => {
96
+ if (query.length === standardIdHexLength) {
97
+ return cbk(null, {channels: [], nodes: []});
98
+ }
99
+
100
+ if (!!getKey.value) {
101
+ return getNode({
102
+ lnd,
103
+ public_key: getKey.value.public_key,
104
+ },
105
+ (err, res) => {
106
+ if (!!err) {
107
+ return cbk(err);
108
+ }
109
+
110
+ return cbk(null, {
111
+ channels: res.channels,
112
+ nodes: [{
113
+ alias: res.alias,
114
+ color: res.color,
115
+ features: res.features,
116
+ public_key: getKey.value.public_key,
117
+ sockets: res.sockets,
118
+ updated_at: res.last_updated,
119
+ }],
120
+ });
121
+ });
122
+ }
123
+
124
+ return getNetworkGraph({lnd}, cbk);
125
+ }],
82
126
 
83
127
  // Get blockchain height
84
128
  getHeight: ['validate', ({}, cbk) => getHeight({lnd}, cbk)],
@@ -16,6 +16,7 @@ const {pemAsDer} = require('./../encryption');
16
16
  is_nospend: <Restrict Credentials To Non-Spending Permissions Bool>
17
17
  is_readonly: <Restrict Credentials To Read-Only Permissions Bool>
18
18
  logger: <Winston Logger Object> ({info}) => ()
19
+ [methods]: [<Allow Specific Method String>]
19
20
  [node]: <Node Name String>
20
21
  }
21
22
 
@@ -81,6 +82,7 @@ module.exports = (args, cbk) => {
81
82
  is_nospend: args.is_nospend,
82
83
  is_readonly: args.is_readonly,
83
84
  logger: args.logger,
85
+ methods: args.methods,
84
86
  node: args.node,
85
87
  },
86
88
  cbk);
@@ -12,6 +12,7 @@ const {grantAccess} = require('ln-service');
12
12
  const {restrictMacaroon} = require('ln-service');
13
13
  const {returnResult} = require('asyncjs-util');
14
14
 
15
+ const credentialRestrictions = require('./credential_restrictions');
15
16
  const {decryptCiphertext} = require('./../encryption');
16
17
  const {derAsPem} = require('./../encryption');
17
18
  const getCert = require('./get_cert');
@@ -38,6 +39,7 @@ const socket = 'localhost:10009';
38
39
  [is_readonly]: <Restrict Credentials To Read-Only Permissions Bool>
39
40
  [key]: <Encrypt to Public Key DER Hex String>
40
41
  [logger]: <Winston Logger Object>
42
+ [methods]: [<Allow Specific Method String>]
41
43
  [node]: <Node Name String> // Defaults to default local mainnet node creds
42
44
  }
43
45
 
@@ -199,8 +201,14 @@ module.exports = (args, cbk) => {
199
201
  'macaroon',
200
202
  ({credentials, macaroon}, cbk) =>
201
203
  {
204
+ const {allow} = credentialRestrictions({
205
+ is_nospend: args.is_nospend,
206
+ is_readonly: args.is_readonly,
207
+ methods: args.methods,
208
+ });
209
+
202
210
  // Exit early when readonly credentials are not requested
203
- if (!args.is_readonly && !args.is_nospend) {
211
+ if (!allow) {
204
212
  return cbk(null, {macaroon});
205
213
  }
206
214
 
@@ -210,9 +218,12 @@ module.exports = (args, cbk) => {
210
218
  socket: credentials.socket,
211
219
  });
212
220
 
213
- const permissions = !!args.is_readonly ? readPerms : noSpendPerms;
214
-
215
- return grantAccess({lnd, permissions}, cbk);
221
+ return grantAccess({
222
+ lnd,
223
+ methods: allow.methods,
224
+ permissions: allow.permissions,
225
+ },
226
+ cbk);
216
227
  }],
217
228
 
218
229
  // Final credentials with encryption applied
@@ -160,11 +160,48 @@ module.exports = (args, cbk) => {
160
160
  // Parse the amount specified
161
161
  parseAmount: [
162
162
  'getChannels',
163
+ 'getInKey',
163
164
  'getOutKey',
165
+ 'getRemoteChannels',
164
166
  'getToKey',
165
167
  'outPeer',
166
- ({getChannels, getOutKey, getToKey, outPeer}, cbk) =>
168
+ ({
169
+ getChannels,
170
+ getInKey,
171
+ getOutKey,
172
+ getRemoteChannels,
173
+ getToKey,
174
+ outPeer,
175
+ },
176
+ cbk) =>
167
177
  {
178
+ // Calculate the inbound peer inbound liquidity
179
+ const inInbound = getRemoteChannels.channels
180
+ .filter(n => n.partner_public_key === getInKey.public_key)
181
+ .reduce((sum, chan) => {
182
+ // Treat incoming payment as if they were still remote balance
183
+ const inbound = chan.pending_payments.filter(n => !n.is_outgoing);
184
+
185
+ const pending = sumOf(inbound.map(({tokens}) => tokens));
186
+
187
+ return sum + chan.remote_balance + pending;
188
+ },
189
+ Number());
190
+
191
+ // Calculate the inbound peer outbound liquidity
192
+ const inOutbound = getRemoteChannels.channels
193
+ .filter(n => n.partner_public_key === getInKey.public_key)
194
+ .reduce((sum, chan) => {
195
+ // Treat outgoing payment as if they were still local balance
196
+ const outbound = chan.pending_payments
197
+ .filter(n => !!n.is_outgoing);
198
+
199
+ const pending = sumOf(outbound.map(({tokens}) => tokens));
200
+
201
+ return sum + chan.local_balance + pending;
202
+ },
203
+ Number());
204
+
168
205
  // Calculate the outbound peer inbound liquidity
169
206
  const outInbound = getChannels.channels
170
207
  .filter(n => n.partner_public_key === getOutKey.public_key)
@@ -194,6 +231,8 @@ module.exports = (args, cbk) => {
194
231
 
195
232
  // Variables to use in amount
196
233
  const variables = {
234
+ in_inbound: inInbound,
235
+ in_outbound: inOutbound,
197
236
  out_inbound: outInbound,
198
237
  out_liquidity: sumOf(
199
238
  getChannels.channels
package/package.json CHANGED
@@ -23,9 +23,9 @@
23
23
  "bolt03": "1.2.10",
24
24
  "bolt07": "1.7.3",
25
25
  "caporal": "1.4.0",
26
- "cbor": "8.0.0",
26
+ "cbor": "8.0.2",
27
27
  "cert-info": "1.5.1",
28
- "colorette": "2.0.12",
28
+ "colorette": "2.0.14",
29
29
  "crypto-js": "4.1.1",
30
30
  "csv-parse": "4.16.3",
31
31
  "goldengate": "10.4.0",
@@ -35,7 +35,7 @@
35
35
  "inquirer": "8.1.5",
36
36
  "invoices": "2.0.0",
37
37
  "ln-accounting": "5.0.3",
38
- "ln-service": "52.8.0",
38
+ "ln-service": "52.10.2",
39
39
  "ln-sync": "2.0.2",
40
40
  "ln-telegram": "3.3.0",
41
41
  "moment": "2.29.1",
@@ -45,7 +45,7 @@
45
45
  "qrcode-terminal": "0.12.0",
46
46
  "sanitize-filename": "1.6.3",
47
47
  "stats-lite": "2.2.0",
48
- "table": "6.7.1",
48
+ "table": "6.7.2",
49
49
  "telegraf": "4.4.2",
50
50
  "update-notifier": "5.1.0",
51
51
  "window-size": "1.1.1"
@@ -80,5 +80,5 @@
80
80
  "postpublish": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t alexbosworth/balanceofsatoshis --push .",
81
81
  "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/fiat/*.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"
82
82
  },
83
- "version": "11.0.0"
83
+ "version": "11.3.0"
84
84
  }
@@ -83,7 +83,10 @@ module.exports = ({channels, filters, tags, query}) => {
83
83
  const matching = isMatchingFilters({
84
84
  filters: filters || [],
85
85
  variables: {
86
- heights: withPeer.map(n => decodeChanId({channel: n.id}).block_height),
86
+ capacity: sumOf(withPeer.map(n => n.capacity)),
87
+ heights: withPeer.map(n => {
88
+ return decodeChanId({channel: n.id}).block_height;
89
+ }),
87
90
  inbound_liquidity: sumOf(withPeer.map(n => n.remote_balance)),
88
91
  outbound_liquidity: sumOf(withPeer.map(n => n.local_balance)),
89
92
  },
@@ -9,13 +9,17 @@ const {returnResult} = require('asyncjs-util');
9
9
 
10
10
  const {describeParseError} = require('./../display');
11
11
 
12
+ const amountVariables = {btc: 1e8, k: 1e3, m: 1e6, mm: 1e6};
12
13
  const asFormula = n => ({formula: n.slice(0, n.length-67), key: n.slice(-66)});
14
+ const asOutFilter = n => ({out_filter: n.slice(67), key: n.slice(0, 66)});
15
+ const {assign} = Object;
13
16
  const decodePair = n => n.split('/');
14
17
  const flatten = arr => [].concat(...arr);
15
18
  const heightFromId = id => Number(id.split('x').shift());
16
19
  const {isArray} = Array;
17
20
  const isChannel = n => /^\d*x\d*x\d*$/.test(n);
18
21
  const isFormula = n => /(.*)\/0[2-3][0-9A-F]{64}$/gim.test(n);
22
+ const isOutFilter = n => /^0[2-3][0-9A-F]{64}\/(.*)/gim.test(n);
19
23
  const isPair = n => !!n && /^0[2-3][0-9A-F]{64}\/0[2-3][0-9A-F]{64}$/i.test(n);
20
24
  const isPublicKey = n => !!n && /^0[2-3][0-9A-F]{64}$/gim.test(n);
21
25
  const {keys} = Object;
@@ -108,6 +112,11 @@ module.exports = (args, cbk) => {
108
112
  return asFormula(id);
109
113
  }
110
114
 
115
+ // Exit early when the id is an out filter
116
+ if (isOutFilter(id)) {
117
+ return asOutFilter(id);
118
+ }
119
+
111
120
  // Exit early when the id is a public key
112
121
  if (isPublicKey(id)) {
113
122
  return {node: {from_public_key: id}};
@@ -161,16 +170,96 @@ module.exports = (args, cbk) => {
161
170
 
162
171
  // Get the block height for use in formulas
163
172
  getHeight: ['sortedAvoids', ({sortedAvoids}, cbk) => {
164
- const formulas = sortedAvoids.filter(n => n.formula);
173
+ const hasFormula = !!sortedAvoids.find(n => !!n.formula);
174
+ const hasOutFilter = !!sortedAvoids.find(n => !!n.out_filter);
165
175
 
166
176
  // Exit early when there are no formulas
167
- if (!formulas.length) {
177
+ if (!hasFormula && !hasOutFilter) {
168
178
  return cbk();
169
179
  }
170
180
 
171
181
  return getHeight({lnd: args.lnd}, cbk);
172
182
  }],
173
183
 
184
+ // Get out filter avoids
185
+ getOutFilterIgnores: [
186
+ 'getHeight',
187
+ 'sortedAvoids',
188
+ ({getHeight, sortedAvoids}, cbk) =>
189
+ {
190
+ const filters = sortedAvoids
191
+ .filter(n => !!n.out_filter)
192
+ .map(n => ({formula: n.out_filter, key: n.key}));
193
+
194
+ return asyncMap(filters, ({formula, key}, cbk) => {
195
+ return getNode({lnd: args.lnd, public_key: key}, (err, res) => {
196
+ // Exit early when the node in question is unknown
197
+ if (isArray(err) && err.slice().shift() === 404) {
198
+ return cbk(null, []);
199
+ }
200
+
201
+ if (!!err) {
202
+ return cbk(err);
203
+ }
204
+
205
+ const outboundAvoids = res.channels
206
+ .map(({capacity, id, policies}) => {
207
+ const height = heightFromId(id);
208
+ const outPolicy = policies.find(n => n.public_key === key);
209
+ const peerPolicy = policies.find(n => n.public_key !== key);
210
+
211
+ if (!outPolicy || !peerPolicy) {
212
+ return;
213
+ }
214
+
215
+ const parser = new Parser();
216
+ const variables = {};
217
+
218
+ assign(variables, amountVariables);
219
+
220
+ assign(variables, {
221
+ capacity,
222
+ height,
223
+ age: getHeight.current_block_height - height,
224
+ base_fee: Number(outPolicy.base_fee_mtokens) || Number(),
225
+ fee_rate: outPolicy.fee_rate || Number(),
226
+ });
227
+
228
+ keys(variables).forEach(key => {
229
+ parser.setVariable(key.toLowerCase(), variables[key]);
230
+ parser.setVariable(key.toUpperCase(), variables[key]);
231
+
232
+ return;
233
+ });
234
+
235
+ const parsed = parser.parse(formula);
236
+
237
+ if (!!parsed.error) {
238
+ return {error: describeParseError({error: parsed.error})};
239
+ }
240
+
241
+ if (parsed.result === false) {
242
+ return;
243
+ }
244
+
245
+ return {
246
+ from_public_key: key,
247
+ to_public_key: peerPolicy.public_key,
248
+ };
249
+ });
250
+
251
+ const {error} = outboundAvoids.find(n => !!n && !!n.error) || {};
252
+
253
+ if (!!error) {
254
+ return cbk([400, 'InvalidAvoidDirective', {error, formula}]);
255
+ }
256
+
257
+ return cbk(null, outboundAvoids.filter(n => !!n));
258
+ });
259
+ },
260
+ cbk);
261
+ }],
262
+
174
263
  // Get formula avoids
175
264
  getFormulaIgnores: [
176
265
  'getHeight',
@@ -181,12 +270,17 @@ module.exports = (args, cbk) => {
181
270
 
182
271
  return asyncMap(formulas, ({formula, key}, cbk) => {
183
272
  return getNode({lnd: args.lnd, public_key: key}, (err, res) => {
273
+ // Exit early when the node in question is unknown
274
+ if (isArray(err) && err.slice().shift() === 404) {
275
+ return cbk(null, []);
276
+ }
277
+
184
278
  if (!!err) {
185
279
  return cbk(err);
186
280
  }
187
281
 
188
282
  const inboundAvoids = res.channels
189
- .map(({id, policies}) => {
283
+ .map(({capacity, id, policies}) => {
190
284
  const height = heightFromId(id);
191
285
  const inPolicy = policies.find(n => n.public_key !== key);
192
286
 
@@ -195,13 +289,17 @@ module.exports = (args, cbk) => {
195
289
  }
196
290
 
197
291
  const parser = new Parser();
292
+ const variables = {};
198
293
 
199
- const variables = {
294
+ assign(variables, amountVariables);
295
+
296
+ assign(variables, {
297
+ capacity,
200
298
  height,
201
299
  age: getHeight.current_block_height - height,
202
300
  base_fee: Number(inPolicy.base_fee_mtokens) || Number(),
203
301
  fee_rate: inPolicy.fee_rate || Number(),
204
- };
302
+ });
205
303
 
206
304
  keys(variables).forEach(key => {
207
305
  parser.setVariable(key.toLowerCase(), variables[key]);
@@ -263,11 +361,13 @@ module.exports = (args, cbk) => {
263
361
  combinedIgnores: [
264
362
  'getChannelIgnores',
265
363
  'getFormulaIgnores',
364
+ 'getOutFilterIgnores',
266
365
  'getQueryIgnores',
267
366
  'sortedAvoids',
268
367
  ({
269
368
  getChannelIgnores,
270
369
  getFormulaIgnores,
370
+ getOutFilterIgnores,
271
371
  getQueryIgnores,
272
372
  sortedAvoids,
273
373
  },
@@ -276,6 +376,7 @@ module.exports = (args, cbk) => {
276
376
  const ignore = [
277
377
  flatten(getChannelIgnores),
278
378
  flatten(getFormulaIgnores),
379
+ flatten(getOutFilterIgnores),
279
380
  getQueryIgnores,
280
381
  sortedAvoids.map(n => n.node).filter(n => !!n),
281
382
  ];
@@ -163,9 +163,33 @@ module.exports = ({lnd}, cbk) => {
163
163
  return cbk(null, balancedChannelRequests.filter(n => !!n));
164
164
  }],
165
165
 
166
+ // Filter out incoming opens that were already accepted
167
+ unacceptedOpens: ['incomingOpens', ({incomingOpens}, cbk) => {
168
+ const asyncFilter = require('async/filter');
169
+ const {getPayment} = require('ln-service');
170
+ return asyncFilter(incomingOpens, (incoming, cbk) => {
171
+ const {id} = parsePaymentRequest({request: incoming.accept_request});
172
+
173
+ return getPayment({id, lnd}, (err, res) => {
174
+ // An unknown payment means the open was not ack'ed
175
+ if (!!err && err.slice().shift() === 404) {
176
+ return cbk(null, true);
177
+ }
178
+
179
+ if (!!err) {
180
+ return cbk(err);
181
+ }
182
+
183
+ // If an accept request was not paid nothing happened yet
184
+ return cbk(null, !!res.is_failed);
185
+ });
186
+ },
187
+ cbk);
188
+ }],
189
+
166
190
  // Final set of active balanced open requests
167
- opens: ['incomingOpens', ({incomingOpens}, cbk) => {
168
- return cbk(null, {incoming: incomingOpens});
191
+ opens: ['unacceptedOpens', ({unacceptedOpens}, cbk) => {
192
+ return cbk(null, {incoming: unacceptedOpens});
169
193
  }],
170
194
  },
171
195
  returnResult({reject, resolve, of: 'opens'}, cbk));
@@ -43,6 +43,14 @@ module.exports = (args, cbk) => {
43
43
  return cbk([400, 'ExpectedLoggerToManageRebalance'])
44
44
  }
45
45
 
46
+ if (isArray(args.max_fee)) {
47
+ return cbk([400, 'ExpectedSingleMaxFeeValue']);
48
+ }
49
+
50
+ if (isArray(args.max_fee_rate)) {
51
+ return cbk([400, 'ExpectedSingleMaxFeeValue']);
52
+ }
53
+
46
54
  if (!args.lnd) {
47
55
  return cbk([400, 'ExpectedLndToManageRebalance']);
48
56
  }
@@ -85,8 +93,8 @@ module.exports = (args, cbk) => {
85
93
  in_through: args.in_through,
86
94
  lnd: args.lnd,
87
95
  logger: args.logger,
88
- max_fee: args.max_fee,
89
- max_fee_rate: args.max_fee_rate,
96
+ max_fee: Number(args.max_fee) || undefined,
97
+ max_fee_rate: Number(args.max_fee_rate) || undefined,
90
98
  max_rebalance: args.max_rebalance,
91
99
  out_filters: args.out_filters,
92
100
  out_inbound: args.out_inbound,
@@ -0,0 +1,74 @@
1
+ const {test} = require('@alexbosworth/tap');
2
+
3
+ const method = require('./../../lnd/credential_restrictions');
4
+
5
+ const tests = [
6
+ {
7
+ args: {},
8
+ description: 'No restrictions results in no allow elements',
9
+ expected: {},
10
+ },
11
+ {
12
+ args: {is_nospend: true},
13
+ description: 'No spend results in nospend permissions',
14
+ expected: {
15
+ allow: {
16
+ methods: [],
17
+ permissions: [
18
+ 'address:read',
19
+ 'address:write',
20
+ 'info:read',
21
+ 'info:write',
22
+ 'invoices:read',
23
+ 'invoices:write',
24
+ 'macaroon:read',
25
+ 'message:read',
26
+ 'offchain:read',
27
+ 'onchain:read',
28
+ 'peers:read',
29
+ 'peers:write',
30
+ 'signer:read',
31
+ ],
32
+ },
33
+ },
34
+ },
35
+ {
36
+ args: {is_readonly: true},
37
+ description: 'Readonly results in read permissions',
38
+ expected: {
39
+ allow: {
40
+ methods: [],
41
+ permissions: [
42
+ 'address:read',
43
+ 'info:read',
44
+ 'invoices:read',
45
+ 'macaroon:read',
46
+ 'message:read',
47
+ 'offchain:read',
48
+ 'onchain:read',
49
+ 'peers:read',
50
+ 'signer:read',
51
+ ],
52
+ },
53
+ },
54
+ },
55
+ {
56
+ args: {methods: ['getWalletInfo']},
57
+ description: 'Readonly results in read permissions',
58
+ expected: {allow: {methods: ['getWalletInfo'], permissions: []}},
59
+ },
60
+ ];
61
+
62
+ tests.forEach(({args, description, error, expected}) => {
63
+ return test(description, async ({end, strictSame, throws}) => {
64
+ if (!!error) {
65
+ throws(() => method(args), new Error(error), 'Got expected error');
66
+ } else {
67
+ const res = method(args);
68
+
69
+ strictSame(res, expected, 'Got expected result');
70
+ }
71
+
72
+ return end();
73
+ });
74
+ });
@@ -1,3 +1,5 @@
1
+ const EventEmitter = require('events');
2
+
1
3
  const {createSignedRequest} = require('invoices');
2
4
  const {createUnsignedRequest} = require('invoices');
3
5
  const sign = require('secp256k1').ecdsaSign;
@@ -107,6 +109,15 @@ const makeArgs = overrides => {
107
109
  });
108
110
  },
109
111
  },
112
+ router: {
113
+ trackPaymentV2: ({}) => {
114
+ const emitter = new EventEmitter();
115
+
116
+ process.nextTick(() => emitter.emit('data', {status: 'FAILED'}));
117
+
118
+ return emitter;
119
+ },
120
+ },
110
121
  },
111
122
  };
112
123
 
@@ -1,194 +0,0 @@
1
- const asyncAuto = require('async/auto');
2
- const {decodePaymentRequest} = require('ln-service');
3
- const {getHeight} = require('ln-service');
4
- const moment = require('moment');
5
- const {payViaPaymentRequest} = require('ln-service');
6
- const {returnResult} = require('asyncjs-util');
7
-
8
- const {authenticatedLnd} = require('./../lnd');
9
- const {decryptPayload} = require('./../encryption');
10
- const {exchanges} = require('./market');
11
- const {pairs} = require('./market');
12
-
13
- const api = 'https://api.suredbits.com/historical/v0/';
14
- const base64ToHex = base64 => Buffer.from(base64, 'base64').toString('hex');
15
- const daysCount = 80;
16
- const defaultMaxFee = 5;
17
- const {isArray} = Array;
18
- const maxCltvDelta = 144 * 30;
19
- const {parse} = JSON;
20
- const pathfindingTimeoutMs = 1000 * 60 * 5;
21
- const titleCase = str => `${str.charAt(0).toUpperCase()}${str.slice(1)}`;
22
-
23
- /** Get historic exchange rates
24
-
25
- {
26
- exchange: <Exchange String>
27
- [fee]: <Desired Maximum Fee Tokens Number>
28
- [node]: <Saved Node Name String>
29
- pair: <Pair String>
30
- request: <Request Function>
31
- }
32
-
33
- @returns via cbk or Promise
34
- {
35
- description: <Price Range Description String>
36
- prices: [<Price on Day String>]
37
- }
38
- */
39
- module.exports = ({exchange, fee, node, pair, request}, cbk) => {
40
- return new Promise((resolve, reject) => {
41
- return asyncAuto({
42
- // Get lnd
43
- getLnd: cbk => authenticatedLnd({node}, cbk),
44
-
45
- // Check arguments
46
- validate: cbk => {
47
- if (!exchanges.find(n => n === exchange)) {
48
- return cbk([400, 'ExpectedKnownExchangeToGetPriceData']);
49
- }
50
-
51
- if (fee !== undefined && !fee) {
52
- return cbk([400, 'ExpectedNonZeroMaxFeeToGetPriceData']);
53
- }
54
-
55
- if (!pairs[pair]) {
56
- return cbk([400, 'ExpectedKnownPairToGetPriceData']);
57
- }
58
-
59
- if (!pairs[pair].find(n => n === exchange)) {
60
- return cbk([400, 'UnsupportedExchange', {supported: pairs[pair]}]);
61
- }
62
-
63
- return cbk();
64
- },
65
-
66
- // Get price data
67
- getPrices: ['validate', ({}, cbk) => {
68
- const year = moment().format('Y');
69
-
70
- return request({
71
- json: true,
72
- url: `${api}${exchange}/${pair.toUpperCase()}/${year}/daily`,
73
- },
74
- (err, r, json) => {
75
- if (!!err) {
76
- return cbk([503, 'UnexpectedErrorGettingHistoricalPrices', {err}]);
77
- }
78
-
79
- if (!r) {
80
- return cbk([503, 'ExpectedResponseWhenGettingHistoricalPrices']);
81
- }
82
-
83
- if (r.statusCode !== 200) {
84
- return cbk([503, 'UnexpectedStatusCodeGettingHistoricalPrices'])
85
- }
86
-
87
- if (!json) {
88
- return cbk([503, 'ExpectedResponseDataFromHistoricalPriceApi']);
89
- }
90
-
91
- if (!json.encryptedData) {
92
- return cbk([503, 'ExpectedEncryptedHistoricalPriceData']);
93
- }
94
-
95
- if (!json.invoice) {
96
- return cbk([503, 'ExpectedPaymentRequestForHistoricalPriceData']);
97
- }
98
-
99
- try {
100
- return cbk(null, {
101
- encrypted: base64ToHex(json.encryptedData),
102
- request: json.invoice,
103
- });
104
- } catch (err) {
105
- return cbk([503, 'UnexpectedDataFromHistoricalPriceApi', {err}]);
106
- }
107
- });
108
- }],
109
-
110
- // Decode request
111
- decodedRequest: ['getLnd', 'getPrices', ({getLnd, getPrices}, cbk) => {
112
- return decodePaymentRequest({
113
- lnd: getLnd.lnd,
114
- request: getPrices.request,
115
- },
116
- cbk);
117
- }],
118
-
119
- // Get height
120
- getHeight: ['decodedRequest', 'getLnd', ({getLnd}, cbk) => {
121
- return getHeight({lnd: getLnd.lnd}, cbk);
122
- }],
123
-
124
- // Purchase preimage needed to decrypt price data
125
- payInvoice: [
126
- 'decodedRequest',
127
- 'getHeight',
128
- 'getLnd',
129
- 'getPrices',
130
- ({decodedRequest, getHeight, getLnd, getPrices}, cbk) =>
131
- {
132
- const {tokens} = decodedRequest;
133
-
134
- // Check that the payment request doesn't require too many tokens
135
- if (tokens > (fee || defaultMaxFee)) {
136
- return cbk([400, 'MaxFeePriceFetchFeeTooLow', {needed_fee: tokens}]);
137
- }
138
-
139
- return payViaPaymentRequest({
140
- lnd: getLnd.lnd,
141
- max_fee: (fee || defaultMaxFee) - tokens,
142
- max_timeout_height: getHeight.current_block_height + maxCltvDelta,
143
- pathfinding_timeout: pathfindingTimeoutMs,
144
- request: getPrices.request,
145
- },
146
- cbk);
147
- }],
148
-
149
- // Decrypt price data
150
- prices: ['getPrices', 'payInvoice', ({getPrices, payInvoice}, cbk) => {
151
- const {encrypted} = getPrices;
152
- const {secret} = payInvoice;
153
-
154
- try {
155
- const {payload} = decryptPayload({encrypted, secret});
156
-
157
- if (!isArray(parse(payload)) || !parse(payload).length) {
158
- return cbk([503, 'ExpectedArrayOfPricesInPayload']);
159
- }
160
-
161
- const prices = parse(payload).slice(-daysCount);
162
-
163
- if (!!prices.find(n => n.pair !== pair.toUpperCase())) {
164
- return cbk([503, 'ExpectedPriceForSpecifiedHistoricPair']);
165
- }
166
-
167
- if (!!prices.find(n => !n.price)) {
168
- return cbk([503, 'ExpectedPriceForHistoricPriceQuote']);
169
- }
170
-
171
- if (!!prices.find(n => !n.timestamp)) {
172
- return cbk([503, 'ExpectedTimestampForHistoricPriceQuote']);
173
- }
174
-
175
- const [end] = prices.slice().reverse();
176
- const [start] = prices;
177
-
178
- const day0 = `${moment(start.timestamp).format('L')}`;
179
- const fin = `${moment(end.timestamp).format('L')}`;
180
- const last = `Last Price: ${end.price}`
181
- const market = `${titleCase(exchange)} ${start.pair}`;
182
-
183
- return cbk(null, {
184
- description: `${market} from ${day0} to ${fin}. ${last}.`,
185
- prices: prices.map(({price}) => price),
186
- });
187
- } catch (err) {
188
- return cbk([503, 'FailedToDecryptHistoricPricesData', {err}]);
189
- }
190
- }],
191
- },
192
- returnResult({reject, resolve, of: 'prices'}, cbk));
193
- });
194
- };