balanceofsatoshis 24.1.0 → 24.2.1

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,14 @@
1
1
  # Versions
2
2
 
3
+ ## 24.2.1
4
+
5
+ - `open`: Adhere to BIP 69 when ordering funding outputs with equal amounts
6
+
7
+ ## 24.2.0
8
+
9
+ - Add support for LND 0.21.4 and LND 0.20.5
10
+ - `pay`, `probe`: Add support for tags in `--out`, `--out-filter`
11
+
3
12
  ## 24.1.0
4
13
 
5
14
  - `fees`: Add `--repeat-interval-ms` to show or set the fees again each time an
package/README.md CHANGED
@@ -7,8 +7,8 @@ Commands for working with LND balances.
7
7
 
8
8
  Supported LND versions:
9
9
 
10
- - v0.21.0-beta to v0.21.3-beta
11
- - v0.20.0-beta to v0.20.4-beta
10
+ - v0.21.0-beta to v0.21.4-beta
11
+ - v0.20.0-beta to v0.20.5-beta
12
12
 
13
13
  ## Install
14
14
 
package/bos CHANGED
@@ -14,7 +14,6 @@ const {writeFile} = require('fs');
14
14
  const importLazy = require('import-lazy')(require);
15
15
 
16
16
  const accounting = importLazy('ln-accounting');
17
- const fetch = importLazy('@alexbosworth/node-fetch');
18
17
  const fiat = importLazy('@alexbosworth/fiat');
19
18
  const lnService = importLazy('ln-service');
20
19
  const lnSync = importLazy('ln-sync');
@@ -1517,6 +1516,10 @@ prog
1517
1516
  // Pay a payment request, probing first
1518
1517
  .command('pay', 'Pay a payment request, probing first')
1519
1518
  .help('Blinded path requests are probed to the introduction node, 1 path')
1519
+ .help('--out can take a tag to make the first hop through any tagged peer')
1520
+ .help('--out-filter vars: CAPACITY/HEIGHTS/INBOUND_LIQUIDITY')
1521
+ .help('--out-filter vars: OUTBOUND_LIQUIDITY/PENDING_PAYMENTS')
1522
+ .help('--out-filter vars: INBOUND_BASE_FEE/INBOUND_FEE_RATE')
1520
1523
  .argument('<request>', 'Payment Request')
1521
1524
  .option('--avoid <avoid>', 'Avoid forwarding via node/chan/tag', REPEATABLE)
1522
1525
  .option('--avoid-high-fee-routes', 'Avoid evaluating too-high fee routes')
@@ -1526,7 +1529,8 @@ prog
1526
1529
  .option('--message <message>', 'Attach text message to payment')
1527
1530
  .option('--no-color', 'Mute all colors')
1528
1531
  .option('--node <node_name>', 'Node to use to pay payment request')
1529
- .option('--out <public_key>', 'Make first hop through peer', REPEATABLE)
1532
+ .option('--out <peer_or_tag>', 'Make first hop through peer/tag', REPEATABLE)
1533
+ .option('--out-filter <out_filter>', 'Filter outbound peer/tag', REPEATABLE)
1530
1534
  .action((args, options, logger) => {
1531
1535
  return new Promise(async (resolve, reject) => {
1532
1536
  try {
@@ -1545,6 +1549,7 @@ prog
1545
1549
  max_paths: options.maxPaths,
1546
1550
  message: options.message,
1547
1551
  out: flatten([options.out].filter(n => !!n)),
1552
+ out_filters: flatten([options.outFilter].filter(n => !!n)),
1548
1553
  request: args.request,
1549
1554
  },
1550
1555
  responses.returnObject({logger, reject, resolve}));
@@ -1637,6 +1642,10 @@ prog
1637
1642
  .command('probe', 'Check if a payment request is sendable')
1638
1643
  .help('Simulate paying a payment request without actually paying it')
1639
1644
  .help('Blinded path requests are probed to the introduction node, 1 path')
1645
+ .help('--out can take a tag to make the first hop through any tagged peer')
1646
+ .help('--out-filter vars: CAPACITY/HEIGHTS/INBOUND_LIQUIDITY')
1647
+ .help('--out-filter vars: OUTBOUND_LIQUIDITY/PENDING_PAYMENTS')
1648
+ .help('--out-filter vars: INBOUND_BASE_FEE/INBOUND_FEE_RATE')
1640
1649
  .argument('<to>', 'Payment request or node public key')
1641
1650
  .argument('[amount]', 'Amount to probe, default: request amount')
1642
1651
  .option('--avoid <avoid>', 'Avoid forwarding via node/chan/tag', REPEATABLE)
@@ -1647,7 +1656,8 @@ prog
1647
1656
  .option('--max-paths <max>', 'Maximum paths to use for find-max', INT, 1)
1648
1657
  .option('--no-color', 'Mute all colors')
1649
1658
  .option('--node <node_name>', 'Node to use for payment request check')
1650
- .option('--out <public_key>', 'Make first hop through peer', REPEATABLE)
1659
+ .option('--out <peer_or_tag>', 'Make first hop through peer/tag', REPEATABLE)
1660
+ .option('--out-filter <out_filter>', 'Filter outbound peer/tag', REPEATABLE)
1651
1661
  .action((args, options, logger) => {
1652
1662
  return new Promise(async (resolve, reject) => {
1653
1663
  try {
@@ -1667,6 +1677,7 @@ prog
1667
1677
  max_fee: options.maxFee,
1668
1678
  max_paths: options.maxPaths,
1669
1679
  out: flatten([options.out].filter(n => !!n)),
1680
+ out_filters: flatten([options.outFilter].filter(n => !!n)),
1670
1681
  request: args.to.length !== 66 ? args.to : undefined,
1671
1682
  tokens: args.amount || undefined,
1672
1683
  },
@@ -1,14 +1,12 @@
1
1
  const {stringify} = require('querystring');
2
2
 
3
- const {AbortController} = require('abort-controller');
4
-
5
3
  const encodeQs = qs => !qs ? '' : '?' + stringify(qs);
6
4
  const timeoutSignals = new WeakMap();
7
5
 
8
6
  /** Derive a request function that uses fetch to simulate request behavior
9
7
 
10
8
  {
11
- fetch: <Node Fetch Function>
9
+ fetch: <Fetch Function>
12
10
  }
13
11
 
14
12
  @returns
@@ -40,7 +40,7 @@ const tokensAsMillitokens = tok => (BigInt(tok) * BigInt(1e3)).toString();
40
40
  type: <Message To Final Destination Type Number String>
41
41
  value: <Message To Final Destination Raw Value Hex Encoded String>
42
42
  }]
43
- [outgoing_channel]: <Outgoing Channel Id String>
43
+ [outgoing_channels]: [<Outgoing Channel Id String>]
44
44
  [paths]: [{
45
45
  base_fee_mtokens: <Accumulated Base Fee Millitokens String>
46
46
  cltv_delta: <Accumulated CLTV Expiry Delta Number>
@@ -176,7 +176,7 @@ module.exports = (args, cbk) => {
176
176
  max_fee_mtokens: strictMaxFee || undefined,
177
177
  max_timeout_height: args.max_timeout_height,
178
178
  messages: args.messages,
179
- outgoing_channel: args.outgoing_channel,
179
+ outgoing_channels: args.outgoing_channels,
180
180
  path_timeout_ms: pathTimeoutMs,
181
181
  paths: args.paths,
182
182
  payment: args.payment,
@@ -0,0 +1,235 @@
1
+ const asyncAuto = require('async/auto');
2
+ const asyncMapSeries = require('async/mapSeries');
3
+ const {findKey} = require('ln-sync');
4
+ const {getChannels} = require('ln-service');
5
+ const {getIdentity} = require('ln-service');
6
+ const {getNode} = require('ln-service');
7
+ const {returnResult} = require('asyncjs-util');
8
+
9
+ const findPeerMatch = require('./../peers/find_peer_match');
10
+ const {getTags} = require('./../tags');
11
+
12
+ const flatten = arr => [].concat(...arr);
13
+ const {isArray} = Array;
14
+ const isFeeFilter = n => /\binbound_(base_fee|fee_rate)\b/i.test(n);
15
+ const uniq = arr => Array.from(new Set(arr));
16
+
17
+ /** Get the peers to route out through, expanding tags and applying filters
18
+
19
+ A tag query expands to all of the tagged peers that satisfy the filters
20
+
21
+ {
22
+ [filters]: [<Outbound Peer Filter Formula String>]
23
+ [fs]: {
24
+ getFile: <Read File Contents Function> (path, cbk) => {}
25
+ }
26
+ lnd: <Authenticated LND API Object>
27
+ out: [<Out Through Peer Public Key, Alias, or Tag String>]
28
+ }
29
+
30
+ @returns via cbk or Promise
31
+ {
32
+ public_keys: [<Out Through Peer Public Key Hex String>]
33
+ }
34
+ */
35
+ module.exports = (args, cbk) => {
36
+ return new Promise((resolve, reject) => {
37
+ return asyncAuto({
38
+ // Check arguments
39
+ validate: cbk => {
40
+ if (!!args.filters && !isArray(args.filters)) {
41
+ return cbk([400, 'ExpectedArrayOfOutFiltersToGetOutPeers']);
42
+ }
43
+
44
+ if (!args.lnd) {
45
+ return cbk([400, 'ExpectedAuthenticatedLndToGetOutPeers']);
46
+ }
47
+
48
+ if (!isArray(args.out)) {
49
+ return cbk([400, 'ExpectedArrayOfOutQueriesToGetOutPeers']);
50
+ }
51
+
52
+ if (!!args.out.filter(n => !n).length) {
53
+ return cbk([400, 'ExpectedNonEmptyOutQueriesToGetOutPeers']);
54
+ }
55
+
56
+ // Filters narrow down out peers so there must be out peers to filter
57
+ if (!args.out.length && !!args.filters && !!args.filters.length) {
58
+ return cbk([400, 'NoPeerMatchesFoundToSatisfyOutboundFilter']);
59
+ }
60
+
61
+ return cbk();
62
+ },
63
+
64
+ // Get channels to find peers
65
+ getChannels: ['validate', ({}, cbk) => {
66
+ // Exit early when there are no out peers
67
+ if (!args.out.length) {
68
+ return cbk();
69
+ }
70
+
71
+ return getChannels({lnd: args.lnd}, cbk);
72
+ }],
73
+
74
+ // Get the identity key to find the remote policies of channels
75
+ getIdentity: ['validate', ({}, cbk) => {
76
+ // Exit early when there are no filters that use fee variables
77
+ if (!(args.filters || []).some(isFeeFilter)) {
78
+ return cbk();
79
+ }
80
+
81
+ return getIdentity({lnd: args.lnd}, cbk);
82
+ }],
83
+
84
+ // Get the set of tags
85
+ getTags: ['validate', ({}, cbk) => {
86
+ // Exit early when there are no out peers or no way to read tags
87
+ if (!args.fs || !args.out.length) {
88
+ return cbk(null, {tags: []});
89
+ }
90
+
91
+ return getTags({fs: args.fs}, cbk);
92
+ }],
93
+
94
+ // Get the remote policies of channels for fee filter variables
95
+ getPolicies: ['getIdentity', ({getIdentity}, cbk) => {
96
+ // Exit early when there are no filters that use fee variables
97
+ if (!getIdentity) {
98
+ return cbk(null, []);
99
+ }
100
+
101
+ const id = getIdentity.public_key;
102
+
103
+ return getNode({lnd: args.lnd, public_key: id}, (err, res) => {
104
+ if (!!err) {
105
+ return cbk(err);
106
+ }
107
+
108
+ const policies = res.channels
109
+ .map(channel => channel.policies.find(n => n.public_key !== id))
110
+ .filter(n => !!n)
111
+ .map(policy => ({
112
+ base_fee_mtokens: policy.base_fee_mtokens,
113
+ fee_rate: policy.fee_rate,
114
+ public_key: policy.public_key,
115
+ }));
116
+
117
+ return cbk(null, policies);
118
+ });
119
+ }],
120
+
121
+ // Find the peers for each out query
122
+ getPeers: [
123
+ 'getChannels',
124
+ 'getPolicies',
125
+ 'getTags',
126
+ ({getChannels, getPolicies, getTags}, cbk) =>
127
+ {
128
+ // Exit early when there are no out peers
129
+ if (!args.out.length) {
130
+ return cbk(null, []);
131
+ }
132
+
133
+ const active = getChannels.channels.filter(n => !!n.is_active);
134
+ const filters = args.filters || [];
135
+
136
+ const peerKeys = uniq(active.map(n => n.partner_public_key));
137
+
138
+ // Check if a peer satisfies the filters
139
+ const isMatch = key => findPeerMatch({
140
+ filters,
141
+ channels: active,
142
+ nodes: [key],
143
+ policies: getPolicies,
144
+ });
145
+
146
+ return asyncMapSeries(args.out.map(n => String(n)), (query, cbk) => {
147
+ // Find tags that match on id or on alias
148
+ const tagMatches = getTags.tags.filter(tag => {
149
+ const alias = tag.alias || String();
150
+
151
+ const isAliasMatch = alias.toLowerCase() === query.toLowerCase();
152
+ const isIdMatch = tag.id.startsWith(query);
153
+
154
+ return isAliasMatch || isIdMatch;
155
+ });
156
+
157
+ // Limit tag matches to tags with relevant peers
158
+ const matches = tagMatches.filter(tag => {
159
+ return (tag.nodes || []).some(n => peerKeys.includes(n));
160
+ });
161
+
162
+ const [tag, ...otherTags] = matches;
163
+
164
+ if (!!otherTags.length) {
165
+ return cbk([400, 'MultipleTagMatchesFoundForOutPeer', {matches}]);
166
+ }
167
+
168
+ // A recognized tag must not fall through to an alias search
169
+ if (!tag && !!tagMatches.length) {
170
+ return cbk([400, 'NoActivePeersFoundForOutTag', {query}]);
171
+ }
172
+
173
+ // Exit early when the query is a tag, expanding to its peers
174
+ if (!!tag) {
175
+ const results = uniq(tag.nodes)
176
+ .filter(n => peerKeys.includes(n))
177
+ .map(key => isMatch(key));
178
+
179
+ const {failure} = results.find(n => !!n.failure) || {};
180
+
181
+ if (!!failure) {
182
+ return cbk([400, 'FailedToParseFilter', failure]);
183
+ }
184
+
185
+ const keys = results.map(n => n.match).filter(n => !!n);
186
+
187
+ if (!keys.length) {
188
+ return cbk([400, 'NoPeerMatchesFoundToSatisfyOutboundFilter']);
189
+ }
190
+
191
+ return cbk(null, keys);
192
+ }
193
+
194
+ // The query is not a tag so look for a public key or alias match
195
+ return findKey({
196
+ query,
197
+ channels: getChannels.channels,
198
+ lnd: args.lnd,
199
+ },
200
+ (err, res) => {
201
+ if (!!err) {
202
+ return cbk(err);
203
+ }
204
+
205
+ const key = res.public_key.toLowerCase();
206
+
207
+ // Exit early when there are no filters to apply
208
+ if (!filters.length) {
209
+ return cbk(null, [key]);
210
+ }
211
+
212
+ const {failure, match} = isMatch(key);
213
+
214
+ if (!!failure) {
215
+ return cbk([400, 'FailedToParseFilter', failure]);
216
+ }
217
+
218
+ if (!match) {
219
+ return cbk([400, 'NoPeerMatchesFoundToSatisfyOutboundFilter']);
220
+ }
221
+
222
+ return cbk(null, [match]);
223
+ });
224
+ },
225
+ cbk);
226
+ }],
227
+
228
+ // Final set of out peers
229
+ peers: ['getPeers', ({getPeers}, cbk) => {
230
+ return cbk(null, {public_keys: uniq(flatten(getPeers))});
231
+ }],
232
+ },
233
+ returnResult({reject, resolve, of: 'peers'}, cbk));
234
+ });
235
+ };
@@ -121,7 +121,7 @@ module.exports = (args, cbk) => {
121
121
  in_through: args.in_through,
122
122
  lnd: args.lnd,
123
123
  logger: args.logger,
124
- out_through: args.out_through,
124
+ out_through: [args.out_through].filter(n => !!n),
125
125
  request: args.request,
126
126
  timeout_minutes: args.timeout_minutes,
127
127
  tokens: args.tokens,
package/network/pay.js CHANGED
@@ -1,6 +1,5 @@
1
1
  const asyncAuto = require('async/auto');
2
2
  const asyncMap = require('async/map');
3
- const {findKey} = require('ln-sync');
4
3
  const {formatTokens} = require('ln-sync');
5
4
  const {getChannels} = require('ln-service');
6
5
  const {getIdentity} = require('ln-service');
@@ -14,6 +13,7 @@ const {describeRoute} = require('./../display');
14
13
  const {describeRoutingFailure} = require('./../display');
15
14
  const {getIgnores} = require('./../routing');
16
15
  const {getTags} = require('./../tags');
16
+ const getOutPeers = require('./get_out_peers');
17
17
  const probeDestination = require('./probe_destination');
18
18
 
19
19
  const cltvDeltaBuffer = 3;
@@ -37,7 +37,8 @@ const withTotalFee = ({fee, ...res}) => ({total_fee: fee, ...res});
37
37
  max_fee: <Max Fee Tokens Number>
38
38
  max_paths: <Maximum Paths Number>
39
39
  [message]: <Message String>
40
- out: [<Out Through Peer With Public Key Hex String>]
40
+ out: [<Out Through Peer With Public Key, Alias, or Tag String>]
41
+ [out_filters]: [<Out Through Peer Filter Formula String>]
41
42
  request: <BOLT 11 Payment Request String>
42
43
  }
43
44
  */
@@ -113,8 +114,11 @@ module.exports = (args, cbk) => {
113
114
 
114
115
  // Find public keys to pay out through
115
116
  getOuts: ['validate', ({}, cbk) => {
116
- return asyncMap(args.out, (query, cbk) => {
117
- return findKey({query, lnd: args.lnd}, cbk);
117
+ return getOutPeers({
118
+ filters: args.out_filters,
119
+ fs: args.fs,
120
+ lnd: args.lnd,
121
+ out: args.out,
118
122
  },
119
123
  cbk);
120
124
  }],
@@ -133,15 +137,16 @@ module.exports = (args, cbk) => {
133
137
  getBaseIgnores: [
134
138
  'getChannels',
135
139
  'getIdentity',
140
+ 'getOuts',
136
141
  'getTags',
137
- ({getChannels, getIdentity, getTags}, cbk) =>
142
+ ({getChannels, getIdentity, getOuts, getTags}, cbk) =>
138
143
  {
139
144
  // Exit early when there are no avoids
140
145
  if (!args.avoid.length) {
141
146
  return cbk(null, {ignore: []});
142
147
  }
143
148
 
144
- const [out] = args.out || [];
149
+ const [out, ...otherOuts] = getOuts.public_keys;
145
150
 
146
151
  return getIgnores({
147
152
  avoid: args.avoid,
@@ -149,7 +154,7 @@ module.exports = (args, cbk) => {
149
154
  in_through: args.in_through,
150
155
  lnd: args.lnd,
151
156
  logger: args.logger,
152
- out_through: out,
157
+ out_through: !otherOuts.length ? out : undefined,
153
158
  public_key: getIdentity.public_key,
154
159
  tags: getTags.tags,
155
160
  },
@@ -168,14 +173,14 @@ module.exports = (args, cbk) => {
168
173
  }
169
174
 
170
175
  // Exit early when there is no outbound restriction
171
- if (!getOuts.length) {
176
+ if (!getOuts.public_keys.length) {
172
177
  return cbk(null, {ignore: getBaseIgnores.ignore});
173
178
  }
174
179
 
175
180
  return getSyntheticOutIgnores({
176
181
  ignore: getBaseIgnores.ignore,
177
182
  lnd: args.lnd,
178
- out: getOuts.map(n => n.public_key),
183
+ out: getOuts.public_keys,
179
184
  },
180
185
  cbk);
181
186
  }],
@@ -183,19 +188,14 @@ module.exports = (args, cbk) => {
183
188
  // Make single-path payment
184
189
  singlePathPay: [
185
190
  'getBaseIgnores',
186
- 'getOuts', ({getBaseIgnores, getOuts}, cbk) =>
191
+ 'getOuts',
192
+ ({getBaseIgnores, getOuts}, cbk) =>
187
193
  {
188
194
  // Exit early when doing a multi-path
189
195
  if (args.max_paths !== singlePath) {
190
196
  return cbk();
191
197
  }
192
198
 
193
- const [outThrough, otherKey] = getOuts.map(n => n.public_key);
194
-
195
- if (!!otherKey) {
196
- return cbk([501, 'CannotPayThroughMultipleOutPeersOnSinglePath']);
197
- }
198
-
199
199
  return probeDestination({
200
200
  fs: args.fs,
201
201
  ignore: getBaseIgnores.ignore,
@@ -206,7 +206,7 @@ module.exports = (args, cbk) => {
206
206
  lnd: args.lnd,
207
207
  max_fee: args.max_fee,
208
208
  message: args.message,
209
- out_through: outThrough,
209
+ out_through: getOuts.public_keys,
210
210
  request: args.request,
211
211
  },
212
212
  cbk);
package/network/probe.js CHANGED
@@ -1,7 +1,5 @@
1
1
  const asyncAuto = require('async/auto');
2
- const asyncMap = require('async/map');
3
2
  const asyncWhilst = require('async/whilst');
4
- const {findKey} = require('ln-sync');
5
3
  const {formatTokens} = require('ln-sync');
6
4
  const {getChannel} = require('ln-service');
7
5
  const {getChannels} = require('ln-service');
@@ -19,6 +17,7 @@ const {describeRoutingFailure} = require('./../display');
19
17
  const {getIcons} = require('./../display');
20
18
  const {getIgnores} = require('./../routing');
21
19
  const {getTags} = require('./../tags');
20
+ const getOutPeers = require('./get_out_peers');
22
21
  const probeDestination = require('./probe_destination');
23
22
 
24
23
  const defaultFinalCltvDelta = 144;
@@ -47,7 +46,8 @@ const withTotalFee = ({fee, ...res}) => ({total_fee: fee, ...res});
47
46
  logger: <Winston Logger Object>
48
47
  max_fee: <Max Fee Tokens Number>
49
48
  [max_paths]: <Maximum Probe Paths Number>
50
- out: [<Out Through Peer With Public Key Hex String>]
49
+ out: [<Out Through Peer With Public Key, Alias, or Tag String>]
50
+ [out_filters]: [<Out Through Peer Filter Formula String>]
51
51
  [request]: <BOLT 11 Encoded Payment Request String>
52
52
  [timeout_minutes]: <Stop Searching For Routes After N Minutes Number>
53
53
  [tokens]: <Tokens Amount String>
@@ -148,8 +148,11 @@ module.exports = (args, cbk) => {
148
148
 
149
149
  // Find public keys to pay out through
150
150
  getOuts: ['validate', ({}, cbk) => {
151
- return asyncMap(args.out, (query, cbk) => {
152
- return findKey({query, lnd: args.lnd}, cbk);
151
+ return getOutPeers({
152
+ filters: args.out_filters,
153
+ fs: args.fs,
154
+ lnd: args.lnd,
155
+ out: args.out,
153
156
  },
154
157
  cbk);
155
158
  }],
@@ -197,15 +200,16 @@ module.exports = (args, cbk) => {
197
200
  getBaseIgnores: [
198
201
  'getChannels',
199
202
  'getIdentity',
203
+ 'getOuts',
200
204
  'getTags',
201
- ({getChannels, getIdentity, getTags}, cbk) =>
205
+ ({getChannels, getIdentity, getOuts, getTags}, cbk) =>
202
206
  {
203
207
  // Exit early when there are no avoids
204
208
  if (!args.avoid.length) {
205
209
  return cbk(null, {ignore: []});
206
210
  }
207
211
 
208
- const [out] = args.out || [];
212
+ const [out, ...otherOuts] = getOuts.public_keys;
209
213
 
210
214
  return getIgnores({
211
215
  avoid: args.avoid,
@@ -213,7 +217,7 @@ module.exports = (args, cbk) => {
213
217
  in_through: args.in_through,
214
218
  lnd: args.lnd,
215
219
  logger: args.logger,
216
- out_through: out,
220
+ out_through: !otherOuts.length ? out : undefined,
217
221
  public_key: getIdentity.public_key,
218
222
  tags: getTags.tags,
219
223
  },
@@ -232,14 +236,14 @@ module.exports = (args, cbk) => {
232
236
  }
233
237
 
234
238
  // Exit early when there is no outbound restriction
235
- if (!getOuts.length) {
239
+ if (!getOuts.public_keys.length) {
236
240
  return cbk(null, {ignore: getBaseIgnores.ignore});
237
241
  }
238
242
 
239
243
  return getSyntheticOutIgnores({
240
244
  ignore: getBaseIgnores.ignore,
241
245
  lnd: args.lnd,
242
- out: getOuts.map(n => n.public_key),
246
+ out: getOuts.public_keys,
243
247
  },
244
248
  cbk);
245
249
  }],
@@ -256,13 +260,6 @@ module.exports = (args, cbk) => {
256
260
  return cbk();
257
261
  }
258
262
 
259
- // Exit early when probing on a single path
260
- if (getOuts.length > singlePath) {
261
- return cbk([501, 'MultipleOutPeersNotSupportedWithSinglePath']);
262
- }
263
-
264
- const [outThrough] = getOuts.map(n => n.public_key);
265
-
266
263
  return probeDestination({
267
264
  tokens,
268
265
  destination: args.destination,
@@ -273,7 +270,7 @@ module.exports = (args, cbk) => {
273
270
  lnd: args.lnd,
274
271
  logger: args.logger,
275
272
  max_fee: args.max_fee,
276
- out_through: outThrough,
273
+ out_through: getOuts.public_keys,
277
274
  request: args.request,
278
275
  },
279
276
  cbk);
@@ -36,6 +36,7 @@ const feeBuffer = fees => fees * 2;
36
36
  const {floor} = Math;
37
37
  const fromKeyType = '34349339';
38
38
  const htlcOutputSize = 43;
39
+ const {isArray} = Array;
39
40
  const isBlindedHop = hop => !!hop.encrypted_data && !hop.path_key;
40
41
  const keySendPreimageType = '5482373484';
41
42
  const makeNonce = () => randomBytes(32).toString('hex');
@@ -49,6 +50,7 @@ const {now} = Date;
49
50
  const pathChannel = hop => isBlindedHop(hop) ? blinded : hop.channel;
50
51
  const rate = n => n.commit_transaction_fee / (n.commit_transaction_weight / 4);
51
52
  const signatureType = '34349337';
53
+ const singlePeer = 1;
52
54
  const tokAsMtok = tokens => (BigInt(tokens || 0) * BigInt(1e3)).toString();
53
55
 
54
56
  /** Determine if a destination can be paid by probing it
@@ -78,7 +80,7 @@ const tokAsMtok = tokens => (BigInt(tokens || 0) * BigInt(1e3)).toString();
78
80
  type: <Additional Message To Final Destination Type Number String>
79
81
  value: <Message To Final Destination Raw Value Hex Encoded String>
80
82
  }]
81
- [out_through]: <Out Through Peer With Public Key Hex String>
83
+ [out_through]: [<Out Through Any Peer With Public Key Hex String>]
82
84
  [request]: <Payment Request String>
83
85
  [timeout_minutes]: <Stop Searching For Route After N Minutes Number>
84
86
  [tokens]: <Tokens Number>
@@ -109,13 +111,24 @@ module.exports = (args, cbk) => {
109
111
  return cbk([400, "ExpectedLoggerToProbeDestination"]);
110
112
  }
111
113
 
114
+ if (!!args.out_through && !isArray(args.out_through)) {
115
+ return cbk([400, 'ExpectedArrayOfOutThroughPeersToProbe']);
116
+ }
117
+
118
+ const outPeers = args.out_through || [];
119
+
120
+ // Finding the maximum is limited by a single outgoing channel
121
+ if (!!args.find_max && outPeers.length > singlePeer) {
122
+ return cbk([501, 'FindMaxNotSupportedWithMultipleOutThroughPeers']);
123
+ }
124
+
112
125
  return cbk();
113
126
  },
114
127
 
115
128
  // Get channels to determine an outgoing channel id restriction
116
129
  getChannels: ['validate', ({}, cbk) => {
117
130
  // Exit early when there is no need to add an outgoing channel id
118
- if (!args.out_through) {
131
+ if (!args.out_through || !args.out_through.length) {
119
132
  return cbk();
120
133
  }
121
134
 
@@ -395,12 +408,12 @@ module.exports = (args, cbk) => {
395
408
  }
396
409
 
397
410
  const {channels} = getChannels;
398
- const outPeer = args.out_through;
411
+ const outPeers = args.out_through;
399
412
  const tokens = args.tokens || to.tokens || defaultTokens;
400
413
 
401
414
  const withPeer = channels
402
415
  .filter(n => !!n.is_active)
403
- .filter(n => n.partner_public_key === outPeer);
416
+ .filter(n => outPeers.includes(n.partner_public_key));
404
417
 
405
418
  if (!withPeer.length) {
406
419
  return cbk([404, 'NoActiveChannelWithOutgoingPeer']);
@@ -417,6 +430,12 @@ module.exports = (args, cbk) => {
417
430
  return cbk([404, 'NoOutboundPeerWithSufficientBalance']);
418
431
  }
419
432
 
433
+ // Exit early when going out through any channel of multiple peers
434
+ if (outPeers.length > singlePeer) {
435
+ return cbk(null, {ids: withBalance.map(n => n.id)});
436
+ }
437
+
438
+ // A single peer goes out through one channel with enough balance
420
439
  const attribute = 'local_balance';
421
440
 
422
441
  const [channel] = sortBy({attribute, array: withBalance}).sorted;
@@ -427,9 +446,9 @@ module.exports = (args, cbk) => {
427
446
  const fees = channel.commit_transaction_fee + bufferFees;
428
447
  const spendable = channel.local_balance - reserve;
429
448
 
430
- const maxPayable = floor(spendable - feeBuffer(fees));
449
+ const maxPayable = floor(spendable - feeBuffer(fees)) || args.find_max;
431
450
 
432
- return cbk(null, {id: channel.id, max: maxPayable || args.find_max});
451
+ return cbk(null, {ids: [channel.id], max: maxPayable});
433
452
  }],
434
453
 
435
454
  // Log sending towards destination
@@ -502,7 +521,7 @@ module.exports = (args, cbk) => {
502
521
  max_fee: args.max_fee,
503
522
  max_fee_mtokens: args.max_fee_mtokens,
504
523
  mtokens: !BigInt(to.mtokens) ? tokAsMtok(defaultTokens) : to.mtokens,
505
- outgoing_channel: !!outId ? outId.id : undefined,
524
+ outgoing_channels: !!outId ? outId.ids : undefined,
506
525
  paths: to.paths || undefined,
507
526
  payment: to.payment,
508
527
  routes: to.routes,