balanceofsatoshis 25.5.0 → 25.6.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 +8 -0
- package/README.md +5 -2
- package/bos +47 -0
- package/display/describe_routing_failure.js +6 -2
- package/lnurl/get_pay_request.js +8 -3
- package/lnurl/get_pay_terms.js +12 -3
- package/lnurl/parse_url.js +18 -4
- package/network/constants.json +1 -0
- package/network/execute_probe.js +14 -11
- package/network/get_out_channels.js +128 -0
- package/network/get_out_queries.js +102 -0
- package/network/get_stream_invoice.js +95 -0
- package/network/get_stream_route.js +153 -0
- package/network/index.js +2 -0
- package/network/line_writer.js +29 -0
- package/network/pay_stream_route.js +103 -0
- package/network/stream_payment.js +591 -0
- package/package.json +6 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Versions
|
|
2
2
|
|
|
3
|
+
## 25.6.1
|
|
4
|
+
|
|
5
|
+
- Add support for LND 0.21.6 and LND 0.20.7
|
|
6
|
+
|
|
7
|
+
## 25.6.0
|
|
8
|
+
|
|
9
|
+
- `stream`: Add command to stream a payment to a lnurl or lightning address
|
|
10
|
+
|
|
3
11
|
## 25.5.0
|
|
4
12
|
|
|
5
13
|
- `probe`: Add `--ignore-expiry` to probe expired payment requests
|
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.
|
|
11
|
-
- v0.20.0-beta to v0.20.
|
|
10
|
+
- v0.21.0-beta to v0.21.6-beta
|
|
11
|
+
- v0.20.0-beta to v0.20.7-beta
|
|
12
12
|
|
|
13
13
|
## Install
|
|
14
14
|
|
|
@@ -168,6 +168,9 @@ bos send
|
|
|
168
168
|
# Run a server of paid services
|
|
169
169
|
bos server
|
|
170
170
|
|
|
171
|
+
# Stream a payment to a lnurl or lightning address in a series of payments
|
|
172
|
+
bos stream "amount" --lnurl "lnurl/lightning_address"
|
|
173
|
+
|
|
171
174
|
# Connect up to a Telegram bot
|
|
172
175
|
bos telegram
|
|
173
176
|
|
package/bos
CHANGED
|
@@ -1954,6 +1954,53 @@ prog
|
|
|
1954
1954
|
});
|
|
1955
1955
|
})
|
|
1956
1956
|
|
|
1957
|
+
// Stream a payment in a series of payments
|
|
1958
|
+
.command('stream', 'Stream a payment in a series of payments')
|
|
1959
|
+
.help('Each payment is sized to the most a single route can carry')
|
|
1960
|
+
.help('--lnurl is required, it can stream above the max the service takes')
|
|
1961
|
+
.help('--min-payment defaults to 100*k, a smaller remainder is not sent')
|
|
1962
|
+
.help('--out can take a tag to make the first hop through any tagged peer')
|
|
1963
|
+
.help('--out-filter is checked before every payment, skipping failing peers')
|
|
1964
|
+
.help('--out-filter vars: CAPACITY/HEIGHTS/INBOUND_LIQUIDITY')
|
|
1965
|
+
.help('--out-filter vars: OUTBOUND_LIQUIDITY/PENDING_PAYMENTS')
|
|
1966
|
+
.help('--out-filter vars: INBOUND_BASE_FEE/INBOUND_FEE_RATE')
|
|
1967
|
+
.help('Amounts can take m/k variables: 5*m for 5 million, 250*k = 0.0025')
|
|
1968
|
+
.argument('<amount>', 'Total amount to stream')
|
|
1969
|
+
.option('--avoid <avoid>', 'Avoid forwarding via node/chan/tag', REPEATABLE)
|
|
1970
|
+
.option('--lnurl <url>', 'Lnurl/lightning address', STRING, undefined, true)
|
|
1971
|
+
.option('--max-fee <max_fee>', 'Maximum total fee to pay', STRING, '1337')
|
|
1972
|
+
.option('--max-fee-rate <ppm>', 'Max fee rate for all payments', INT, 1000)
|
|
1973
|
+
.option('--max-payment <amount>', 'Largest single payment to make', STRING)
|
|
1974
|
+
.option('--min-payment <amount>', 'Smallest single payment to make', STRING)
|
|
1975
|
+
.option('--no-color', 'Mute all colors')
|
|
1976
|
+
.option('--node <node_name>', 'Node to stream from')
|
|
1977
|
+
.option('--out <peer_or_tag>', 'Make first hop through peer/tag', REPEATABLE)
|
|
1978
|
+
.option('--out-filter <out_filter>', 'Filter outbound peer/tag', REPEATABLE)
|
|
1979
|
+
.action((args, options, logger) => {
|
|
1980
|
+
return new Promise(async (resolve, reject) => {
|
|
1981
|
+
try {
|
|
1982
|
+
return network.streamPayment({
|
|
1983
|
+
logger,
|
|
1984
|
+
amount: args.amount,
|
|
1985
|
+
avoid: flatten([options.avoid].filter(n => !!n)),
|
|
1986
|
+
fs: {getFile: readFile},
|
|
1987
|
+
lnd: (await lndForNode(logger, options.node)).lnd,
|
|
1988
|
+
lnurl: options.lnurl,
|
|
1989
|
+
max_fee: options.maxFee,
|
|
1990
|
+
max_fee_rate: options.maxFeeRate,
|
|
1991
|
+
max_payment: options.maxPayment,
|
|
1992
|
+
min_payment: options.minPayment,
|
|
1993
|
+
out: flatten([options.out].filter(n => !!n)),
|
|
1994
|
+
out_filters: flatten([options.outFilter].filter(n => !!n)),
|
|
1995
|
+
request: commands.simpleRequest,
|
|
1996
|
+
},
|
|
1997
|
+
responses.returnObject({logger, reject, resolve}));
|
|
1998
|
+
} catch (err) {
|
|
1999
|
+
return logger.error({err}) && reject();
|
|
2000
|
+
}
|
|
2001
|
+
});
|
|
2002
|
+
})
|
|
2003
|
+
|
|
1957
2004
|
// Get a swap API key or inspect an existing one
|
|
1958
2005
|
.command('swap-api-key', 'Purchase a swap API key or inspect a swap API key')
|
|
1959
2006
|
.visible(false)
|
|
@@ -108,6 +108,7 @@ module.exports = ({index, lnd, reason, route, tagged}, cbk) => {
|
|
|
108
108
|
|
|
109
109
|
const {display} = chartAliasForPeer({
|
|
110
110
|
alias: getTo.alias,
|
|
111
|
+
icons: !!tag ? tag.icons : undefined,
|
|
111
112
|
public_key: getTo.id,
|
|
112
113
|
});
|
|
113
114
|
|
|
@@ -120,11 +121,14 @@ module.exports = ({index, lnd, reason, route, tagged}, cbk) => {
|
|
|
120
121
|
'toName',
|
|
121
122
|
({fromName, toName}, cbk) =>
|
|
122
123
|
{
|
|
123
|
-
const
|
|
124
|
+
const hop = route.hops[index];
|
|
125
|
+
|
|
126
|
+
// A failure at the destination is not on a channel out of it
|
|
127
|
+
const at = !hop ? '' : ` on ${hop.channel}`;
|
|
124
128
|
const from = !fromName ? '' : `from ${fromName}`;
|
|
125
129
|
const to = !toName ? '' : ` forwarding to ${toName}`;
|
|
126
130
|
|
|
127
|
-
return cbk(null, {description: `${reason} ${from}
|
|
131
|
+
return cbk(null, {description: `${reason} ${from}${at}${to}`});
|
|
128
132
|
}],
|
|
129
133
|
},
|
|
130
134
|
returnResult({reject, resolve, of: 'description'}, cbk));
|
package/lnurl/get_pay_request.js
CHANGED
|
@@ -3,6 +3,7 @@ const {parsePaymentRequest} = require('ln-service');
|
|
|
3
3
|
const {returnResult} = require('asyncjs-util');
|
|
4
4
|
|
|
5
5
|
const errorStatus = 'ERROR';
|
|
6
|
+
const timeoutMs = 1000 * 60;
|
|
6
7
|
|
|
7
8
|
/** Get a payment request for a LNURL
|
|
8
9
|
|
|
@@ -45,9 +46,13 @@ module.exports = ({hash, mtokens, request, url}, cbk) => {
|
|
|
45
46
|
|
|
46
47
|
// Get the payment request
|
|
47
48
|
getRequest: ['validate', ({}, cbk) => {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
return request({
|
|
50
|
+
url,
|
|
51
|
+
json: true,
|
|
52
|
+
qs: {amount: mtokens},
|
|
53
|
+
timeout: timeoutMs,
|
|
54
|
+
},
|
|
55
|
+
(err, r, json) => {
|
|
51
56
|
if (!!err) {
|
|
52
57
|
return cbk([503, 'FailedToGetPaymentRequestFromService', {err}]);
|
|
53
58
|
}
|
package/lnurl/get_pay_terms.js
CHANGED
|
@@ -15,6 +15,7 @@ const payRequestTag = 'payRequest';
|
|
|
15
15
|
const sha256 = n => createHash('sha256').update(n).digest().toString('hex');
|
|
16
16
|
const sslProtocol = 'https:';
|
|
17
17
|
const textPlain = 'text/plain';
|
|
18
|
+
const timeoutMs = 1000 * 60;
|
|
18
19
|
const utf8AsBuffer = utf8 => Buffer.from(utf8, 'utf8');
|
|
19
20
|
|
|
20
21
|
/** Get payment terms
|
|
@@ -51,7 +52,12 @@ module.exports = ({request, url}, cbk) => {
|
|
|
51
52
|
|
|
52
53
|
// Get payment terms
|
|
53
54
|
getTerms: ['validate', ({}, cbk) => {
|
|
54
|
-
return request({
|
|
55
|
+
return request({
|
|
56
|
+
url,
|
|
57
|
+
json: true,
|
|
58
|
+
timeout: timeoutMs,
|
|
59
|
+
},
|
|
60
|
+
(err, r, json) => {
|
|
55
61
|
if (!!err) {
|
|
56
62
|
return cbk([503, 'FailureGettingLnUrlDataFromUrl', {err}]);
|
|
57
63
|
}
|
|
@@ -100,14 +106,17 @@ module.exports = ({request, url}, cbk) => {
|
|
|
100
106
|
return cbk([503, 'ExpectedMetadataArrayInLnurlResponse', json]);
|
|
101
107
|
}
|
|
102
108
|
|
|
103
|
-
const
|
|
109
|
+
const textEntry = parse(json.metadata)
|
|
104
110
|
.filter(isArray)
|
|
105
111
|
.find(([entry, text]) => entry === textPlain && !!text);
|
|
106
112
|
|
|
107
|
-
|
|
113
|
+
// Exit early when there is no text/plain entry to describe the terms
|
|
114
|
+
if (!textEntry) {
|
|
108
115
|
return cbk([503, 'ExpectedTextPlainEntryInLnurlResponse']);
|
|
109
116
|
}
|
|
110
117
|
|
|
118
|
+
const [, description] = textEntry;
|
|
119
|
+
|
|
111
120
|
if (!isNumber(json.minSendable)) {
|
|
112
121
|
return cbk([503, 'ExpectedNumericValueForMinSendable']);
|
|
113
122
|
}
|
package/lnurl/parse_url.js
CHANGED
|
@@ -3,9 +3,9 @@ const {bech32} = require('bech32');
|
|
|
3
3
|
const asLnurl = n => n.substring(n.startsWith('lightning:') ? 10 : 0);
|
|
4
4
|
const bech32CharLimit = 2000;
|
|
5
5
|
const {decode} = bech32;
|
|
6
|
-
const isEmail = n => /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,
|
|
6
|
+
const isEmail = n => /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(n);
|
|
7
7
|
const isOnion = n => /.onion$/.test(n);
|
|
8
|
-
const isUsername = n => /^[a-z0-9_
|
|
8
|
+
const isUsername = n => /^[a-z0-9_.-]*$/.test(n);
|
|
9
9
|
const join = arr => arr.join('');
|
|
10
10
|
const parseEmail = email => email.split('@');
|
|
11
11
|
const prefix = 'lnurl';
|
|
@@ -46,7 +46,14 @@ module.exports = ({url}) => {
|
|
|
46
46
|
throw new Error('LnurlOnionUrlsCurrentlyUnsupported');
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
const addressUrl = join([sslProtocol, domain, urlString, username]);
|
|
50
|
+
|
|
51
|
+
// Exit with error when the lightning address does not make a valid URL
|
|
52
|
+
if (!URL.canParse(addressUrl)) {
|
|
53
|
+
throw new Error('ExpectedValidUrlForLightningAddress');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return {url: addressUrl};
|
|
50
57
|
}
|
|
51
58
|
|
|
52
59
|
if (decode(asLnurl(url), bech32CharLimit).prefix !== prefix) {
|
|
@@ -55,5 +62,12 @@ module.exports = ({url}) => {
|
|
|
55
62
|
|
|
56
63
|
const {words} = decode(asLnurl(url), bech32CharLimit);
|
|
57
64
|
|
|
58
|
-
|
|
65
|
+
const decoded = wordsAsUtf8(words);
|
|
66
|
+
|
|
67
|
+
// Exit with error when the lnurl does not hold a valid URL
|
|
68
|
+
if (!URL.canParse(decoded)) {
|
|
69
|
+
throw new Error('ExpectedValidUrlInLnurl');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return {url: decoded};
|
|
59
73
|
};
|
package/network/constants.json
CHANGED
package/network/execute_probe.js
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
const asyncAuto = require('async/auto');
|
|
2
|
-
const asyncMapSeries = require('async/mapSeries');
|
|
3
|
-
const {getNode} = require('ln-service');
|
|
4
2
|
const {returnResult} = require('asyncjs-util');
|
|
5
|
-
const {subscribeToProbe} = require('ln-service');
|
|
6
3
|
const {subscribeToProbeForRoute} = require('ln-service');
|
|
7
4
|
|
|
8
|
-
const {describeConfidence} = require('./../display');
|
|
9
5
|
const {describeRoute} = require('./../display');
|
|
10
6
|
const {describeRoutingFailure} = require('./../display');
|
|
7
|
+
const lineWriter = require('./line_writer');
|
|
11
8
|
|
|
12
9
|
const {now} = Date;
|
|
13
10
|
const minutesAsMs = minutes => 1000 * 60 * minutes;
|
|
@@ -189,10 +186,14 @@ module.exports = (args, cbk) => {
|
|
|
189
186
|
return cbk([400, 'FailedToStartProbeForRoute', {err}]);
|
|
190
187
|
}
|
|
191
188
|
|
|
189
|
+
// Lines are written in the order of events, and all before finishing
|
|
190
|
+
const lines = lineWriter({logger: args.logger});
|
|
191
|
+
|
|
192
192
|
const finished = (err, res) => {
|
|
193
193
|
sub.removeAllListeners();
|
|
194
194
|
|
|
195
|
-
|
|
195
|
+
// Wait for lines that are being written so that they are not lost
|
|
196
|
+
return lines.wait().then(() => cbk(err, res));
|
|
196
197
|
};
|
|
197
198
|
|
|
198
199
|
// Finish without success
|
|
@@ -204,12 +205,12 @@ module.exports = (args, cbk) => {
|
|
|
204
205
|
sub.on('error', err => finished(err));
|
|
205
206
|
|
|
206
207
|
// Log failures encountered while trying to find a route
|
|
207
|
-
sub.on('routing_failure',
|
|
208
|
+
sub.on('routing_failure', failure => {
|
|
208
209
|
if (!!args.log_failure) {
|
|
209
210
|
args.log_failure(null, failure);
|
|
210
211
|
}
|
|
211
212
|
|
|
212
|
-
const
|
|
213
|
+
const described = describeRoutingFailure({
|
|
213
214
|
index: failure.index,
|
|
214
215
|
lnd: args.lnd,
|
|
215
216
|
reason: failure.reason,
|
|
@@ -217,7 +218,7 @@ module.exports = (args, cbk) => {
|
|
|
217
218
|
tagged: args.tagged,
|
|
218
219
|
});
|
|
219
220
|
|
|
220
|
-
return
|
|
221
|
+
return lines.write(described.then(n => ({failure: n.description})));
|
|
221
222
|
});
|
|
222
223
|
|
|
223
224
|
// Finish with successful probe
|
|
@@ -233,16 +234,18 @@ module.exports = (args, cbk) => {
|
|
|
233
234
|
});
|
|
234
235
|
|
|
235
236
|
// Log probing attempts
|
|
236
|
-
sub.on('probing',
|
|
237
|
+
sub.on('probing', ({route}) => {
|
|
237
238
|
attemptedPaths.push(route);
|
|
238
239
|
|
|
239
|
-
const
|
|
240
|
+
const described = describeRoute({
|
|
240
241
|
route,
|
|
241
242
|
lnd: args.lnd,
|
|
242
243
|
tagged: args.tagged || undefined,
|
|
243
244
|
});
|
|
244
245
|
|
|
245
|
-
|
|
246
|
+
const line = described.then(n => ({evaluating: n.description}));
|
|
247
|
+
|
|
248
|
+
return lines.write(line);
|
|
246
249
|
});
|
|
247
250
|
|
|
248
251
|
return;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
const asyncAuto = require('async/auto');
|
|
2
|
+
const asyncMapSeries = require('async/mapSeries');
|
|
3
|
+
const asyncReflect = require('async/reflect');
|
|
4
|
+
const {getChannels} = require('ln-service');
|
|
5
|
+
const {returnResult} = require('asyncjs-util');
|
|
6
|
+
|
|
7
|
+
const getOutPeers = require('./get_out_peers');
|
|
8
|
+
|
|
9
|
+
const flatten = arr => [].concat(...arr);
|
|
10
|
+
const inactiveTagError = 'NoActivePeersFoundForOutTag';
|
|
11
|
+
const {isArray} = Array;
|
|
12
|
+
const noMatchError = 'NoPeerMatchesFoundToSatisfyOutboundFilter';
|
|
13
|
+
const uniq = arr => Array.from(new Set(arr));
|
|
14
|
+
|
|
15
|
+
/** Get the channels to route out through, applying out filters
|
|
16
|
+
|
|
17
|
+
Out peers that fail the filters or have no active channel are left out
|
|
18
|
+
|
|
19
|
+
{
|
|
20
|
+
[filters]: [<Outbound Peer Filter Formula String>]
|
|
21
|
+
[fs]: {
|
|
22
|
+
getFile: <Read File Contents Function> (path, cbk) => {}
|
|
23
|
+
}
|
|
24
|
+
lnd: <Authenticated LND API Object>
|
|
25
|
+
out: [<Out Through Peer With Public Key, Alias, or Tag String>]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@returns via cbk or Promise
|
|
29
|
+
{
|
|
30
|
+
[channels]: [<Out Through Standard Format Channel Id String>]
|
|
31
|
+
public_keys: [<Out Through Peer Public Key Hex String>]
|
|
32
|
+
}
|
|
33
|
+
*/
|
|
34
|
+
module.exports = ({filters, fs, lnd, out}, cbk) => {
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
return asyncAuto({
|
|
37
|
+
// Check arguments
|
|
38
|
+
validate: cbk => {
|
|
39
|
+
if (!lnd) {
|
|
40
|
+
return cbk([400, 'ExpectedAuthenticatedLndToGetOutChannels']);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!isArray(out)) {
|
|
44
|
+
return cbk([400, 'ExpectedArrayOfOutPeersToGetOutChannels']);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Filters narrow down out peers so there must be out peers to filter
|
|
48
|
+
if (!out.length && !!filters && !!filters.length) {
|
|
49
|
+
return cbk([400, noMatchError]);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return cbk();
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
// Find public keys to pay out through for each out peer or tag
|
|
56
|
+
getOuts: ['validate', ({}, cbk) => {
|
|
57
|
+
return asyncMapSeries(out, (query, cbk) => {
|
|
58
|
+
return asyncReflect(getOutPeers)({
|
|
59
|
+
filters,
|
|
60
|
+
fs,
|
|
61
|
+
lnd,
|
|
62
|
+
out: [query],
|
|
63
|
+
},
|
|
64
|
+
cbk);
|
|
65
|
+
},
|
|
66
|
+
cbk);
|
|
67
|
+
}],
|
|
68
|
+
|
|
69
|
+
// Out peers that pass the filters, leaving out the ones that do not and
|
|
70
|
+
// tags with no active peers
|
|
71
|
+
peers: ['getOuts', ({getOuts}, cbk) => {
|
|
72
|
+
const errors = getOuts.filter(n => !!n.error).map(n => n.error);
|
|
73
|
+
const leftOut = [inactiveTagError, noMatchError];
|
|
74
|
+
|
|
75
|
+
// An error other than an out peer being left out is passed back
|
|
76
|
+
const [err] = errors.filter(([, msg]) => !leftOut.includes(msg));
|
|
77
|
+
|
|
78
|
+
if (!!err) {
|
|
79
|
+
return cbk(err);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const found = getOuts.filter(n => !n.error).map(n => n.value);
|
|
83
|
+
|
|
84
|
+
return cbk(null, uniq(flatten(found.map(n => n.public_keys))));
|
|
85
|
+
}],
|
|
86
|
+
|
|
87
|
+
// Get channels with the out peers
|
|
88
|
+
getChannels: ['peers', ({}, cbk) => {
|
|
89
|
+
// Exit early when there is no outbound restriction
|
|
90
|
+
if (!out.length) {
|
|
91
|
+
return cbk();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return getChannels({lnd}, cbk);
|
|
95
|
+
}],
|
|
96
|
+
|
|
97
|
+
// Channels to go out through, with the out peers that have them
|
|
98
|
+
channels: ['getChannels', 'peers', ({getChannels, peers}, cbk) => {
|
|
99
|
+
// Exit early when there is no outbound restriction
|
|
100
|
+
if (!out.length) {
|
|
101
|
+
return cbk(null, {public_keys: []});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const withPeer = getChannels.channels
|
|
105
|
+
.filter(n => !!n.is_active)
|
|
106
|
+
.filter(n => peers.includes(n.partner_public_key));
|
|
107
|
+
|
|
108
|
+
// Exit with error when the filters left no out peer to go through
|
|
109
|
+
if (!withPeer.length && !!filters && !!filters.length) {
|
|
110
|
+
return cbk([400, noMatchError]);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Exit with error when no out peer has an active channel
|
|
114
|
+
if (!withPeer.length) {
|
|
115
|
+
return cbk([404, 'NoActiveChannelWithOutgoingPeer']);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const keys = withPeer.map(n => n.partner_public_key);
|
|
119
|
+
|
|
120
|
+
return cbk(null, {
|
|
121
|
+
channels: withPeer.map(n => n.id),
|
|
122
|
+
public_keys: peers.filter(key => keys.includes(key)),
|
|
123
|
+
});
|
|
124
|
+
}],
|
|
125
|
+
},
|
|
126
|
+
returnResult({reject, resolve, of: 'channels'}, cbk));
|
|
127
|
+
});
|
|
128
|
+
};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
const asyncAuto = require('async/auto');
|
|
2
|
+
const asyncMapSeries = require('async/mapSeries');
|
|
3
|
+
const {findKey} = require('ln-sync');
|
|
4
|
+
const {returnResult} = require('asyncjs-util');
|
|
5
|
+
|
|
6
|
+
const asKey = n => !n.public_key ? undefined : n.public_key.toLowerCase();
|
|
7
|
+
const flatten = arr => [].concat(...arr);
|
|
8
|
+
const {isArray} = Array;
|
|
9
|
+
const uniq = arr => Array.from(new Set(arr));
|
|
10
|
+
|
|
11
|
+
/** Get out queries with the aliases of out peers resolved to public keys
|
|
12
|
+
|
|
13
|
+
Tag queries are kept so that the peers of tags can be found again later
|
|
14
|
+
|
|
15
|
+
{
|
|
16
|
+
channels: [{
|
|
17
|
+
partner_public_key: <Peer Public Key Hex String>
|
|
18
|
+
}]
|
|
19
|
+
lnd: <Authenticated LND API Object>
|
|
20
|
+
out: [<Out Through Peer With Public Key, Alias, or Tag String>]
|
|
21
|
+
tags: [{
|
|
22
|
+
[alias]: <Tag Alias String>
|
|
23
|
+
id: <Tag Id Hex String>
|
|
24
|
+
nodes: [<Public Key Hex String>]
|
|
25
|
+
}]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@returns via cbk or Promise
|
|
29
|
+
{
|
|
30
|
+
public_keys: [<Out Peer Public Key Hex String>]
|
|
31
|
+
queries: [<Out Through Peer Public Key or Tag String>]
|
|
32
|
+
}
|
|
33
|
+
*/
|
|
34
|
+
module.exports = ({channels, lnd, out, tags}, cbk) => {
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
return asyncAuto({
|
|
37
|
+
// Check arguments
|
|
38
|
+
validate: cbk => {
|
|
39
|
+
if (!isArray(channels)) {
|
|
40
|
+
return cbk([400, 'ExpectedArrayOfChannelsToGetOutQueries']);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!lnd) {
|
|
44
|
+
return cbk([400, 'ExpectedAuthenticatedLndToGetOutQueries']);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!isArray(out)) {
|
|
48
|
+
return cbk([400, 'ExpectedArrayOfOutPeersToGetOutQueries']);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!isArray(tags)) {
|
|
52
|
+
return cbk([400, 'ExpectedArrayOfTagsToGetOutQueries']);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return cbk();
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
// Out queries with the tags that they match on alias or on id
|
|
59
|
+
outQueries: ['validate', ({}, cbk) => {
|
|
60
|
+
return cbk(null, out.map(n => String(n)).map(query => ({
|
|
61
|
+
query,
|
|
62
|
+
tags: tags.filter(tag => {
|
|
63
|
+
const alias = tag.alias || String();
|
|
64
|
+
|
|
65
|
+
const isAliasMatch = alias.toLowerCase() === query.toLowerCase();
|
|
66
|
+
|
|
67
|
+
return isAliasMatch || tag.id.startsWith(query);
|
|
68
|
+
}),
|
|
69
|
+
})));
|
|
70
|
+
}],
|
|
71
|
+
|
|
72
|
+
// Find the public keys of out peers, other than peers of tags
|
|
73
|
+
getKeys: ['outQueries', ({outQueries}, cbk) => {
|
|
74
|
+
return asyncMapSeries(outQueries, (outQuery, cbk) => {
|
|
75
|
+
// Exit early when the query is a tag, its peers are found later
|
|
76
|
+
if (!!outQuery.tags.length) {
|
|
77
|
+
return cbk(null, {});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return findKey({channels, lnd, query: outQuery.query}, cbk);
|
|
81
|
+
},
|
|
82
|
+
cbk);
|
|
83
|
+
}],
|
|
84
|
+
|
|
85
|
+
// Out queries with peer aliases replaced by peer public keys
|
|
86
|
+
queries: ['getKeys', 'outQueries', ({getKeys, outQueries}, cbk) => {
|
|
87
|
+
const keys = getKeys.map(asKey);
|
|
88
|
+
const peers = channels.map(n => n.partner_public_key);
|
|
89
|
+
const tagged = flatten(outQueries.map(n => n.tags));
|
|
90
|
+
|
|
91
|
+
// Out peers are the peers that are named and the peers in out tags
|
|
92
|
+
const outPeers = keys.concat(flatten(tagged.map(n => n.nodes)));
|
|
93
|
+
|
|
94
|
+
return cbk(null, {
|
|
95
|
+
public_keys: uniq(outPeers.filter(n => peers.includes(n))),
|
|
96
|
+
queries: outQueries.map((n, i) => keys[i] || n.query),
|
|
97
|
+
});
|
|
98
|
+
}],
|
|
99
|
+
},
|
|
100
|
+
returnResult({reject, resolve, of: 'queries'}, cbk));
|
|
101
|
+
});
|
|
102
|
+
};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
const asyncAuto = require('async/auto');
|
|
2
|
+
const asyncReflect = require('async/reflect');
|
|
3
|
+
const {formatTokens} = require('ln-sync');
|
|
4
|
+
const {returnResult} = require('asyncjs-util');
|
|
5
|
+
|
|
6
|
+
const getPayRequest = require('./../lnurl/get_pay_request');
|
|
7
|
+
|
|
8
|
+
const {isArray} = Array;
|
|
9
|
+
const mtokensAsTokens = n => Number(BigInt(n) / BigInt(1e3));
|
|
10
|
+
const serviceDeclinedError = 'ServiceReturnedError';
|
|
11
|
+
|
|
12
|
+
/** Get an invoice from a LNURL service for a payment of a stream
|
|
13
|
+
|
|
14
|
+
{
|
|
15
|
+
hash: <Description Hash Hex String>
|
|
16
|
+
logger: <Winston Logger Object>
|
|
17
|
+
mtokens: <Invoice Amount Millitokens String>
|
|
18
|
+
request: <Request Function>
|
|
19
|
+
url: <Service Callback URL String>
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@returns via cbk or Promise
|
|
23
|
+
{
|
|
24
|
+
[is_amount_too_high]: <Service Turned Down The Amount Bool>
|
|
25
|
+
[request]: <BOLT 11 Payment Request String>
|
|
26
|
+
}
|
|
27
|
+
*/
|
|
28
|
+
module.exports = ({hash, logger, mtokens, request, url}, cbk) => {
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
return asyncAuto({
|
|
31
|
+
// Check arguments
|
|
32
|
+
validate: cbk => {
|
|
33
|
+
if (!hash) {
|
|
34
|
+
return cbk([400, 'ExpectedDescriptionHashToGetStreamInvoice']);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!logger) {
|
|
38
|
+
return cbk([400, 'ExpectedLoggerToGetStreamInvoice']);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!mtokens) {
|
|
42
|
+
return cbk([400, 'ExpectedMillitokensToGetStreamInvoice']);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!request) {
|
|
46
|
+
return cbk([400, 'ExpectedRequestFunctionToGetStreamInvoice']);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!url) {
|
|
50
|
+
return cbk([400, 'ExpectedServiceUrlToGetStreamInvoice']);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return cbk();
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
// The amount of the invoice to show
|
|
57
|
+
amount: ['validate', ({}, cbk) => {
|
|
58
|
+
return cbk(null, formatTokens({tokens: mtokensAsTokens(mtokens)}));
|
|
59
|
+
}],
|
|
60
|
+
|
|
61
|
+
// Ask the service for an invoice
|
|
62
|
+
getRequest: ['amount', ({amount}, cbk) => {
|
|
63
|
+
logger.info({requesting_invoice: amount.display});
|
|
64
|
+
|
|
65
|
+
const pay = {hash, mtokens, request, url};
|
|
66
|
+
|
|
67
|
+
return asyncReflect(getPayRequest)(pay, cbk);
|
|
68
|
+
}],
|
|
69
|
+
|
|
70
|
+
// Note the invoice, or that the service turned down the amount
|
|
71
|
+
invoice: ['amount', 'getRequest', ({amount, getRequest}, cbk) => {
|
|
72
|
+
const {error, value} = getRequest;
|
|
73
|
+
|
|
74
|
+
// Exit early when the service declined to make the invoice
|
|
75
|
+
if (isArray(error) && error[1] === serviceDeclinedError) {
|
|
76
|
+
const [,, details] = error;
|
|
77
|
+
|
|
78
|
+
logger.info({invoice_declined: amount.display, reason: details.err});
|
|
79
|
+
|
|
80
|
+
return cbk(null, {is_amount_too_high: true});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!!error) {
|
|
84
|
+
return cbk(error);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// The request is what the route search probes towards
|
|
88
|
+
logger.info({payment_request: value.request});
|
|
89
|
+
|
|
90
|
+
return cbk(null, {request: value.request});
|
|
91
|
+
}],
|
|
92
|
+
},
|
|
93
|
+
returnResult({reject, resolve, of: 'invoice'}, cbk));
|
|
94
|
+
});
|
|
95
|
+
};
|