paid-services 10.1.0 → 10.2.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 +6 -10
- package/network/constants.json +16 -0
- package/network/get_message_hops.js +64 -10
- package/network/get_path_outbound.js +14 -8
- package/network/make_network_request.js +2 -1
- package/network/request_network_reply.js +49 -14
- package/network/service_network_messages.js +83 -13
- package/network/service_network_requests.js +4 -0
- package/network/subscription_rejection_error.js +49 -0
- package/offers/get_offer_invoice.js +7 -13
- package/offers/request_offer_invoice.js +2 -17
- package/package.json +1 -1
- package/support/get_support_menu.js +7 -24
- package/support/service_support_offer.js +38 -5
- package/network/reply_introduction.js +0 -44
package/CHANGELOG.md
CHANGED
|
@@ -1,26 +1,22 @@
|
|
|
1
1
|
# Versions
|
|
2
2
|
|
|
3
|
+
## Version 10.2.0
|
|
4
|
+
|
|
5
|
+
- `serviceSupportOffer`: Add `start` event with the support offer
|
|
6
|
+
|
|
3
7
|
## Version 10.1.0
|
|
4
8
|
|
|
5
|
-
- `createSupportOffer`: Add `introduction` to start the created offer path
|
|
6
|
-
a connected peer, for a node that is connected only by private channels
|
|
7
|
-
- `getOfferInvoice`: Check the offer before deriving the payer key, so that a
|
|
8
|
-
refused offer does not use up a payer key index
|
|
9
|
-
- `payOfferInvoice`: Refuse invoices that require multi-part payment
|
|
9
|
+
- `createSupportOffer`: Add `introduction` to start the created offer path
|
|
10
10
|
|
|
11
11
|
## Version 10.0.0
|
|
12
12
|
|
|
13
13
|
- `getOfferInvoice`: Add `features` to show the features of the invoice
|
|
14
14
|
- `getOfferInvoice`: Add support for offers with an issuer id and no paths
|
|
15
|
-
- `getOfferInvoice`: Check that an invoice for an offer without an issuer id
|
|
16
|
-
is signed by the final node of the path that the request was sent over
|
|
17
15
|
|
|
18
16
|
### Breaking Changes
|
|
19
17
|
|
|
20
18
|
- `getOfferInvoice`: Refuse offers with an amount in a currency
|
|
21
|
-
- `getOfferInvoice`:
|
|
22
|
-
808, and `payer_key_index` is returned in place of the removed
|
|
23
|
-
`payer_secret`
|
|
19
|
+
- `getOfferInvoice`: Seed derived `payer_key_index` replaces `payer_secret`
|
|
24
20
|
|
|
25
21
|
## Version 9.3.0
|
|
26
22
|
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"accessDeniedMessages": [
|
|
3
|
+
"^cannot determine data format of binary-encoded macaroon",
|
|
4
|
+
"^empty macaroon data",
|
|
5
|
+
"^expected 1 macaroon",
|
|
6
|
+
"^invalid ID$",
|
|
7
|
+
"^permission denied$",
|
|
8
|
+
"^unknown macaroon version",
|
|
9
|
+
"^unmarshal v[12]",
|
|
10
|
+
"^verification failed"
|
|
11
|
+
],
|
|
12
|
+
"codeInvalidArgument": 3,
|
|
13
|
+
"codePermissionDenied": 7,
|
|
14
|
+
"codeUnauthenticated": 16,
|
|
15
|
+
"codeUnimplemented": 12
|
|
16
|
+
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
const asyncAuto = require('async/auto');
|
|
2
2
|
const asyncRetry = require('async/retry');
|
|
3
|
+
const {getChannels} = require('ln-service');
|
|
3
4
|
const {getIdentity} = require('ln-service');
|
|
4
5
|
const {getPeers} = require('ln-service');
|
|
6
|
+
const {getWalletInfo} = require('ln-service');
|
|
5
7
|
const {returnResult} = require('asyncjs-util');
|
|
6
8
|
|
|
7
9
|
const getRelayingRoute = require('./get_relaying_route');
|
|
@@ -19,8 +21,19 @@ const onlyChannelsBits = [66, 67];
|
|
|
19
21
|
payment route found from the destination to self, through nodes that relay
|
|
20
22
|
messages.
|
|
21
23
|
|
|
24
|
+
When a reply is expected back along the same nodes, and self only takes
|
|
25
|
+
messages from peers with a channel, a connected destination is only sent to
|
|
26
|
+
directly when it has a channel with self, since the reply comes back from
|
|
27
|
+
it.
|
|
28
|
+
|
|
29
|
+
The route is found over the channels that are known, the channels of the
|
|
30
|
+
public graph and the channels of self, so a destination that can only be
|
|
31
|
+
reached through unannounced channels of other nodes, as when self is only
|
|
32
|
+
connected through a peer that is itself private, is not supported.
|
|
33
|
+
|
|
22
34
|
{
|
|
23
35
|
destination: <Destination Node Public Key Hex String>
|
|
36
|
+
[is_expecting_reply]: <Reply Comes Back Along the Same Nodes Bool>
|
|
24
37
|
lnd: <Authenticated LND API Object>
|
|
25
38
|
}
|
|
26
39
|
|
|
@@ -29,17 +42,17 @@ const onlyChannelsBits = [66, 67];
|
|
|
29
42
|
hops: [<Message Hop Public Key Hex String>]
|
|
30
43
|
}
|
|
31
44
|
*/
|
|
32
|
-
module.exports = (
|
|
45
|
+
module.exports = (args, cbk) => {
|
|
33
46
|
return new Promise((resolve, reject) => {
|
|
34
47
|
return asyncAuto({
|
|
35
48
|
// Check arguments
|
|
36
49
|
validate: cbk => {
|
|
37
50
|
// The destination key is a string so that it can be compared
|
|
38
|
-
if (!destination || typeof destination !== 'string') {
|
|
51
|
+
if (!args.destination || typeof args.destination !== 'string') {
|
|
39
52
|
return cbk([400, 'ExpectedDestinationToGetMessageHops']);
|
|
40
53
|
}
|
|
41
54
|
|
|
42
|
-
if (!lnd) {
|
|
55
|
+
if (!args.lnd) {
|
|
43
56
|
return cbk([400, 'ExpectedLndToGetMessageHops']);
|
|
44
57
|
}
|
|
45
58
|
|
|
@@ -47,26 +60,67 @@ module.exports = ({destination, lnd}, cbk) => {
|
|
|
47
60
|
},
|
|
48
61
|
|
|
49
62
|
// Node keys are compared in lowercase, as LND gives them
|
|
50
|
-
key: ['validate', ({}, cbk) =>
|
|
63
|
+
key: ['validate', ({}, cbk) => {
|
|
64
|
+
return cbk(null, args.destination.toLowerCase());
|
|
65
|
+
}],
|
|
51
66
|
|
|
52
67
|
// Get the connected peers
|
|
53
|
-
getPeers: ['validate', ({}, cbk) => getPeers({lnd}, cbk)],
|
|
68
|
+
getPeers: ['validate', ({}, cbk) => getPeers({lnd: args.lnd}, cbk)],
|
|
54
69
|
|
|
55
|
-
// A connected destination is
|
|
56
|
-
|
|
70
|
+
// A connected destination is a direct hop unless it needs a channel
|
|
71
|
+
isPeer: ['getPeers', 'key', ({getPeers, key}, cbk) => {
|
|
57
72
|
const peer = getPeers.peers.find(n => n.public_key === key);
|
|
58
73
|
|
|
59
74
|
return cbk(null, !!peer && !hasBit(peer.features, onlyChannelsBits));
|
|
60
75
|
}],
|
|
61
76
|
|
|
77
|
+
// Get the features of self when a reply comes back from the destination
|
|
78
|
+
getFeatures: ['isPeer', ({isPeer}, cbk) => {
|
|
79
|
+
// Exit early when the destination is not sent to directly anyway
|
|
80
|
+
if (!isPeer || !args.is_expecting_reply) {
|
|
81
|
+
return cbk();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return getWalletInfo({lnd: args.lnd}, cbk);
|
|
85
|
+
}],
|
|
86
|
+
|
|
87
|
+
// Get the channels with the destination when the reply needs a channel
|
|
88
|
+
getChannels: ['getFeatures', 'key', ({getFeatures, key}, cbk) => {
|
|
89
|
+
// Exit early when self takes messages from peers without a channel
|
|
90
|
+
if (!getFeatures || !hasBit(getFeatures.features, onlyChannelsBits)) {
|
|
91
|
+
return cbk();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return getChannels({lnd: args.lnd, partner_public_key: key}, cbk);
|
|
95
|
+
}],
|
|
96
|
+
|
|
97
|
+
// A connected destination also has a channel when the reply needs one
|
|
98
|
+
isDirect: ['getChannels', 'isPeer', ({getChannels, isPeer}, cbk) => {
|
|
99
|
+
// Exit early when the channels were not needed
|
|
100
|
+
if (!getChannels) {
|
|
101
|
+
return cbk(null, isPeer);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return cbk(null, !!getChannels.channels.length);
|
|
105
|
+
}],
|
|
106
|
+
|
|
62
107
|
// Get the identity key of self, which routes are found back to
|
|
63
|
-
getIdentity: [
|
|
108
|
+
getIdentity: [
|
|
109
|
+
'getFeatures',
|
|
110
|
+
'isDirect',
|
|
111
|
+
({getFeatures, isDirect}, cbk) =>
|
|
112
|
+
{
|
|
64
113
|
// Exit early when the destination is sent to directly
|
|
65
114
|
if (isDirect) {
|
|
66
115
|
return cbk();
|
|
67
116
|
}
|
|
68
117
|
|
|
69
|
-
|
|
118
|
+
// Exit early when the identity key came with the features
|
|
119
|
+
if (!!getFeatures) {
|
|
120
|
+
return cbk(null, {public_key: getFeatures.public_key});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return getIdentity({lnd: args.lnd}, cbk);
|
|
70
124
|
}],
|
|
71
125
|
|
|
72
126
|
// Find a series of nodes to relay the message
|
|
@@ -95,9 +149,9 @@ module.exports = ({destination, lnd}, cbk) => {
|
|
|
95
149
|
return asyncRetry({errorFilter: isRetry, times: maxAttempts}, cbk => {
|
|
96
150
|
return getRelayingRoute({
|
|
97
151
|
ignore,
|
|
98
|
-
lnd,
|
|
99
152
|
source,
|
|
100
153
|
destination: key,
|
|
154
|
+
lnd: args.lnd,
|
|
101
155
|
peers: getPeers.peers,
|
|
102
156
|
},
|
|
103
157
|
(err, res) => {
|
|
@@ -13,6 +13,7 @@ const edgeChannel = edge => edge.split('x').slice(0, -1).join('x');
|
|
|
13
13
|
along a payment route through nodes that relay onion messages
|
|
14
14
|
|
|
15
15
|
{
|
|
16
|
+
[is_expecting_reply]: <Reply Comes Back Along the Same Nodes Bool>
|
|
16
17
|
lnd: <Authenticated LND API Object>
|
|
17
18
|
path: {
|
|
18
19
|
[introduction_edge]: <Introduction Node Edge Format Channel Id String>
|
|
@@ -25,16 +26,16 @@ const edgeChannel = edge => edge.split('x').slice(0, -1).join('x');
|
|
|
25
26
|
outbound: [<Relaying Node Public Key Hex String>]
|
|
26
27
|
}
|
|
27
28
|
*/
|
|
28
|
-
module.exports = (
|
|
29
|
+
module.exports = (args, cbk) => {
|
|
29
30
|
return new Promise((resolve, reject) => {
|
|
30
31
|
return asyncAuto({
|
|
31
32
|
// Check arguments
|
|
32
33
|
validate: cbk => {
|
|
33
|
-
if (!lnd) {
|
|
34
|
+
if (!args.lnd) {
|
|
34
35
|
return cbk([400, 'ExpectedLndToGetPathOutbound']);
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
if (!path) {
|
|
38
|
+
if (!args.path) {
|
|
38
39
|
return cbk([400, 'ExpectedBlindedPathToGetPathOutbound']);
|
|
39
40
|
}
|
|
40
41
|
|
|
@@ -44,21 +45,21 @@ module.exports = ({lnd, path}, cbk) => {
|
|
|
44
45
|
// Get the channel of the introduction edge
|
|
45
46
|
getChannel: ['validate', ({}, cbk) => {
|
|
46
47
|
// Exit early when the introduction is a node and not an edge
|
|
47
|
-
if (!path.introduction_edge) {
|
|
48
|
+
if (!args.path.introduction_edge) {
|
|
48
49
|
return cbk();
|
|
49
50
|
}
|
|
50
51
|
|
|
51
|
-
const id = edgeChannel(path.introduction_edge);
|
|
52
|
+
const id = edgeChannel(args.path.introduction_edge);
|
|
52
53
|
|
|
53
|
-
return getChannel({id, lnd}, cbk);
|
|
54
|
+
return getChannel({id, lnd: args.lnd}, cbk);
|
|
54
55
|
}],
|
|
55
56
|
|
|
56
57
|
// Determine the introduction node of the path
|
|
57
58
|
introduction: ['getChannel', ({getChannel}, cbk) => {
|
|
58
59
|
try {
|
|
59
60
|
const {introduction} = pathIntroduction({
|
|
60
|
-
path,
|
|
61
61
|
channel: getChannel,
|
|
62
|
+
path: args.path,
|
|
62
63
|
});
|
|
63
64
|
|
|
64
65
|
return cbk(null, introduction);
|
|
@@ -69,7 +70,12 @@ module.exports = ({lnd, path}, cbk) => {
|
|
|
69
70
|
|
|
70
71
|
// Get the hops to relay the message to the introduction node
|
|
71
72
|
getHops: ['introduction', ({introduction}, cbk) => {
|
|
72
|
-
return getMessageHops({
|
|
73
|
+
return getMessageHops({
|
|
74
|
+
destination: introduction,
|
|
75
|
+
is_expecting_reply: args.is_expecting_reply,
|
|
76
|
+
lnd: args.lnd,
|
|
77
|
+
},
|
|
78
|
+
cbk);
|
|
73
79
|
}],
|
|
74
80
|
|
|
75
81
|
// The outbound hops end at the introduction node
|
|
@@ -17,7 +17,8 @@ const peerRequestType = 32768;
|
|
|
17
17
|
const typeNetworkRequest = '32769';
|
|
18
18
|
|
|
19
19
|
/** Make a network request over onion messages, sent over a blinded path of
|
|
20
|
-
the destination, with the response returned over a reply path
|
|
20
|
+
the destination, with the response returned over a reply path back along
|
|
21
|
+
the route of the request
|
|
21
22
|
|
|
22
23
|
The request fails when no response arrives within the `timeout`.
|
|
23
24
|
|
|
@@ -9,6 +9,7 @@ const getPathOutbound = require('./get_path_outbound');
|
|
|
9
9
|
|
|
10
10
|
const defaultTimeoutMs = 1000 * 30;
|
|
11
11
|
const {isArray} = Array;
|
|
12
|
+
const isHops = n => isSafeInteger(n) && n > 0;
|
|
12
13
|
const isPublicKey = n => typeof n === 'string' && /^[0-9A-F]{66}$/i.test(n);
|
|
13
14
|
const {isSafeInteger} = Number;
|
|
14
15
|
const isTimeout = n => isSafeInteger(n) && n > 0;
|
|
@@ -16,13 +17,19 @@ const isTimeout = n => isSafeInteger(n) && n > 0;
|
|
|
16
17
|
/** Send an onion message over a blinded path and wait for its reply
|
|
17
18
|
|
|
18
19
|
The reply is the first message of one of the reply `types` that arrives on
|
|
19
|
-
the reply path of the message
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
the reply path of the message. The request fails when no reply arrives
|
|
21
|
+
within the `timeout`.
|
|
22
|
+
|
|
23
|
+
The reply path goes back along the route of the message, through the nodes
|
|
24
|
+
that relayed it, ending at self, so that the recipient can reach it. When
|
|
25
|
+
self only takes messages from peers with a channel, the message goes out
|
|
26
|
+
over a channel, so that the reply comes back over one. A reply path of more
|
|
27
|
+
than `max_reply_hops` keeps the hops nearest self, and the recipient routes
|
|
28
|
+
to its start.
|
|
22
29
|
|
|
23
30
|
{
|
|
24
|
-
[introduction]: <Reply Path Introduction Peer Public Key Hex String>
|
|
25
31
|
lnd: <Authenticated LND API Object>
|
|
32
|
+
[max_reply_hops]: <Maximum Reply Path Hops Count Number>
|
|
26
33
|
message: {
|
|
27
34
|
type: <Message Payload Record Type Number String>
|
|
28
35
|
value: <Message Payload Hex String>
|
|
@@ -90,8 +97,8 @@ module.exports = (args, cbk) => {
|
|
|
90
97
|
}
|
|
91
98
|
}
|
|
92
99
|
|
|
93
|
-
if (
|
|
94
|
-
return cbk([400, '
|
|
100
|
+
if (args.max_reply_hops !== undefined && !isHops(args.max_reply_hops)) {
|
|
101
|
+
return cbk([400, 'ExpectedMaximumReplyHopsToRequestNetworkReply']);
|
|
95
102
|
}
|
|
96
103
|
|
|
97
104
|
if (args.timeout !== undefined && !isTimeout(args.timeout)) {
|
|
@@ -112,7 +119,38 @@ module.exports = (args, cbk) => {
|
|
|
112
119
|
|
|
113
120
|
// Determine the outbound hops to reach the path introduction node
|
|
114
121
|
getOutbound: ['validate', ({}, cbk) => {
|
|
115
|
-
return getPathOutbound({
|
|
122
|
+
return getPathOutbound({
|
|
123
|
+
is_expecting_reply: true,
|
|
124
|
+
lnd: args.lnd,
|
|
125
|
+
path: args.path,
|
|
126
|
+
},
|
|
127
|
+
cbk);
|
|
128
|
+
}],
|
|
129
|
+
|
|
130
|
+
// The reply path goes back the way the message went, ending at self
|
|
131
|
+
replyPath: [
|
|
132
|
+
'getIdentity',
|
|
133
|
+
'getOutbound',
|
|
134
|
+
({getIdentity, getOutbound}, cbk) =>
|
|
135
|
+
{
|
|
136
|
+
const {outbound} = getOutbound;
|
|
137
|
+
|
|
138
|
+
// The outbound hops end at the introduction node of the path, which
|
|
139
|
+
// is the recipient itself when the path has a single hop
|
|
140
|
+
const isRecipient = args.path.hops.length === 1;
|
|
141
|
+
|
|
142
|
+
const relays = !isRecipient ? outbound : outbound.slice(0, -1);
|
|
143
|
+
|
|
144
|
+
const hops = [].concat(relays).reverse().concat(getIdentity.public_key);
|
|
145
|
+
|
|
146
|
+
// Exit early when the reply path has no more hops than allowed
|
|
147
|
+
if (!args.max_reply_hops || hops.length <= args.max_reply_hops) {
|
|
148
|
+
return cbk(null, {hops});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// The hops nearest self are kept, which the recipient cannot find on
|
|
152
|
+
// its own, and it routes to the start of the reply path
|
|
153
|
+
return cbk(null, {hops: hops.slice(-args.max_reply_hops)});
|
|
116
154
|
}],
|
|
117
155
|
|
|
118
156
|
// Listen to incoming onion messages
|
|
@@ -131,10 +169,10 @@ module.exports = (args, cbk) => {
|
|
|
131
169
|
|
|
132
170
|
// Send the message and wait for the reply
|
|
133
171
|
request: [
|
|
134
|
-
'getIdentity',
|
|
135
172
|
'getOutbound',
|
|
173
|
+
'replyPath',
|
|
136
174
|
'subscribe',
|
|
137
|
-
({
|
|
175
|
+
({getOutbound, replyPath, subscribe}, cbk) =>
|
|
138
176
|
{
|
|
139
177
|
let isFinished = false;
|
|
140
178
|
let timer;
|
|
@@ -218,17 +256,14 @@ module.exports = (args, cbk) => {
|
|
|
218
256
|
});
|
|
219
257
|
});
|
|
220
258
|
|
|
221
|
-
//
|
|
222
|
-
const introduction = (args.introduction || String()).toLowerCase();
|
|
223
|
-
|
|
224
|
-
// Send the message with a reply path back to self
|
|
259
|
+
// Send the message with a reply path back the way it goes
|
|
225
260
|
const sending = sendMessage({
|
|
226
261
|
inbound: args.path.hops,
|
|
227
262
|
key: args.path.key,
|
|
228
263
|
lnd: args.lnd,
|
|
229
264
|
message: args.message,
|
|
230
265
|
outbound: getOutbound.outbound,
|
|
231
|
-
reply:
|
|
266
|
+
reply: replyPath.hops,
|
|
232
267
|
});
|
|
233
268
|
|
|
234
269
|
return sending.catch(err => finished(err));
|
|
@@ -4,11 +4,17 @@ const getPathId = require('./get_path_id');
|
|
|
4
4
|
const getPathOutbound = require('./get_path_outbound');
|
|
5
5
|
const regularReplyBytes = require('./regular_reply_bytes');
|
|
6
6
|
const returnNetworkResponse = require('./return_network_response');
|
|
7
|
+
const subscriptionRejectionError = require('./subscription_rejection_error');
|
|
7
8
|
|
|
9
|
+
const defaultDelayMs = 1000 * 30;
|
|
10
|
+
const endedError = [503, 'NetworkMessagesSubscriptionEnded'];
|
|
8
11
|
const failedReplyBytes = err => [400, 'FailedToGetRegularReplyBytes', {err}];
|
|
9
12
|
const {isArray} = Array;
|
|
13
|
+
const isDelay = n => isSafeInteger(n) && n > 0;
|
|
10
14
|
const isHex = n => typeof n === 'string' && /^([0-9a-f]{2})+$/i.test(n);
|
|
11
15
|
const isLargeOnion = onion => !!onion && onion.length / 2 === largeOnionBytes;
|
|
16
|
+
const {isSafeInteger} = Number;
|
|
17
|
+
const isUnsupported = err => isArray(err) && err[0] === 501;
|
|
12
18
|
const largeOnionBytes = 32834;
|
|
13
19
|
const stoppedError = [503, 'ServiceStoppedBeforeReplyWasSent'];
|
|
14
20
|
|
|
@@ -24,12 +30,16 @@ const stoppedError = [503, 'ServiceStoppedBeforeReplyWasSent'];
|
|
|
24
30
|
message of the regular size. `reply_bytes` gives the most bytes of such a
|
|
25
31
|
reply that fit, or -1 when no reply fits.
|
|
26
32
|
|
|
27
|
-
|
|
28
|
-
|
|
33
|
+
When the stream of messages fails or ends, the `error` listener is told and
|
|
34
|
+
the stream is subscribed to again after `delay`, keeping the message
|
|
35
|
+
listeners. The `end` listener is called, after the `error` listener, only
|
|
36
|
+
when subscribing again cannot help: LND without onion messages, or a
|
|
37
|
+
subscription that LND rejects. After the service stops, a reply that was
|
|
29
38
|
being prepared is not sent, its callback is given an error, and nothing is
|
|
30
39
|
reported to the error listener.
|
|
31
40
|
|
|
32
41
|
{
|
|
42
|
+
[delay]: <Subscribe Again After Delay Milliseconds Number> // Default: 30000
|
|
33
43
|
[ids]: [<Accepted Path Identifier Hex String>]
|
|
34
44
|
lnd: <Authenticated LND API Object>
|
|
35
45
|
}
|
|
@@ -81,7 +91,11 @@ const stoppedError = [503, 'ServiceStoppedBeforeReplyWasSent'];
|
|
|
81
91
|
bytes: <Most Reply Payload Bytes For Regular Size, -1 For None Number>
|
|
82
92
|
}
|
|
83
93
|
*/
|
|
84
|
-
module.exports = ({ids, lnd}) => {
|
|
94
|
+
module.exports = ({delay, ids, lnd}) => {
|
|
95
|
+
if (delay !== undefined && !isDelay(delay)) {
|
|
96
|
+
throw new Error('ExpectedDelayMillisecondsToServiceNetworkMessages');
|
|
97
|
+
}
|
|
98
|
+
|
|
85
99
|
if (!!ids && !isArray(ids)) {
|
|
86
100
|
throw new Error('ExpectedArrayOfPathIdsToServiceNetworkMessages');
|
|
87
101
|
}
|
|
@@ -95,11 +109,12 @@ module.exports = ({ids, lnd}) => {
|
|
|
95
109
|
|
|
96
110
|
const listeners = {};
|
|
97
111
|
const service = {end: () => {}, error: () => {}};
|
|
98
|
-
const sub = subscribeToMessages({lnd});
|
|
99
112
|
|
|
100
113
|
let isStopped = false;
|
|
114
|
+
let sub;
|
|
115
|
+
let timer;
|
|
101
116
|
|
|
102
|
-
// Stop listening, and tell the caller, when
|
|
117
|
+
// Stop listening, and tell the caller, when subscribing again cannot help
|
|
103
118
|
const ended = err => {
|
|
104
119
|
// Exit early when the service was already stopped
|
|
105
120
|
if (isStopped) {
|
|
@@ -111,18 +126,42 @@ module.exports = ({ids, lnd}) => {
|
|
|
111
126
|
// Removing the listeners cancels the subscription
|
|
112
127
|
sub.removeAllListeners();
|
|
113
128
|
|
|
114
|
-
|
|
115
|
-
service.error(err);
|
|
116
|
-
}
|
|
129
|
+
service.error(err);
|
|
117
130
|
|
|
118
131
|
return service.end();
|
|
119
132
|
};
|
|
120
133
|
|
|
121
|
-
//
|
|
122
|
-
|
|
123
|
-
|
|
134
|
+
// Subscribe again after the stream fails, keeping the message listeners
|
|
135
|
+
const failed = err => {
|
|
136
|
+
// Exit early when the service was already stopped
|
|
137
|
+
if (isStopped) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Removing the listeners cancels the subscription, and a stream that
|
|
142
|
+
// sends an end after its error is not heard, so it is subscribed to
|
|
143
|
+
// again once
|
|
144
|
+
sub.removeAllListeners();
|
|
145
|
+
|
|
146
|
+
// The wait to subscribe again starts before the error listener is told,
|
|
147
|
+
// so that a listener that stops the service calls it off
|
|
148
|
+
timer = setTimeout(() => {
|
|
149
|
+
timer = undefined;
|
|
150
|
+
|
|
151
|
+
// Subscribing can throw, when there is nothing to subscribe with
|
|
152
|
+
try {
|
|
153
|
+
return subscribe();
|
|
154
|
+
} catch (err) {
|
|
155
|
+
return ended([503, 'FailedToSubscribeToNetworkMessages', {err}]);
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
delay || defaultDelayMs);
|
|
159
|
+
|
|
160
|
+
return service.error(err);
|
|
161
|
+
};
|
|
124
162
|
|
|
125
|
-
|
|
163
|
+
// Handle a message that arrived on the stream
|
|
164
|
+
const onMessage = received => {
|
|
126
165
|
const {message} = received;
|
|
127
166
|
|
|
128
167
|
// Exit early when there is no message payload
|
|
@@ -262,7 +301,35 @@ module.exports = ({ids, lnd}) => {
|
|
|
262
301
|
reply_bytes: !reply ? undefined : replyBytes,
|
|
263
302
|
});
|
|
264
303
|
});
|
|
265
|
-
}
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
// Subscribe to the stream of messages
|
|
307
|
+
const subscribe = () => {
|
|
308
|
+
sub = subscribeToMessages({lnd});
|
|
309
|
+
|
|
310
|
+
// The stream can close normally, which is subscribed to again
|
|
311
|
+
sub.on('end', () => failed(endedError));
|
|
312
|
+
|
|
313
|
+
sub.on('error', err => {
|
|
314
|
+
// LND without onion messages has no stream to subscribe to again
|
|
315
|
+
if (isUnsupported(err)) {
|
|
316
|
+
return ended(err);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const {error} = subscriptionRejectionError({err});
|
|
320
|
+
|
|
321
|
+
// A rejected subscription would be rejected again
|
|
322
|
+
if (!!error) {
|
|
323
|
+
return ended(error);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return failed([503, 'NetworkMessagesSubscriptionFailed', {err}]);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
sub.on('message_received', onMessage);
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
subscribe();
|
|
266
333
|
|
|
267
334
|
return {
|
|
268
335
|
end: cbk => service.end = cbk,
|
|
@@ -272,6 +339,9 @@ module.exports = ({ids, lnd}) => {
|
|
|
272
339
|
stop: ({}) => {
|
|
273
340
|
isStopped = true;
|
|
274
341
|
|
|
342
|
+
// Subscribing again is called off when it is being waited for
|
|
343
|
+
clearTimeout(timer);
|
|
344
|
+
|
|
275
345
|
return sub.removeAllListeners();
|
|
276
346
|
},
|
|
277
347
|
};
|
|
@@ -18,6 +18,10 @@ const typeNetworkRequest = '32769';
|
|
|
18
18
|
response does not fit, the error is reported to the `error` listener and a
|
|
19
19
|
`[413, 'ResponseTooLarge']` failure is sent in its place.
|
|
20
20
|
|
|
21
|
+
When the stream of messages fails, the `error` listener is told and the
|
|
22
|
+
stream is subscribed to again, as `serviceNetworkMessages` does, so the
|
|
23
|
+
`end` listener is only called when subscribing again cannot help.
|
|
24
|
+
|
|
21
25
|
{
|
|
22
26
|
[ids]: [<Accepted Path Identifier Hex String>]
|
|
23
27
|
lnd: <Authenticated LND API Object>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const {accessDeniedMessages} = require('./constants');
|
|
2
|
+
const {codeInvalidArgument} = require('./constants');
|
|
3
|
+
const {codePermissionDenied} = require('./constants');
|
|
4
|
+
const {codeUnauthenticated} = require('./constants');
|
|
5
|
+
const {codeUnimplemented} = require('./constants');
|
|
6
|
+
|
|
7
|
+
const accessDeniedCodes = [codePermissionDenied, codeUnauthenticated];
|
|
8
|
+
const deniedMessages = accessDeniedMessages.map(n => new RegExp(n));
|
|
9
|
+
const rejectionCodes = [codeInvalidArgument, codeUnimplemented];
|
|
10
|
+
|
|
11
|
+
/** Derive the error for a messages subscription that LND rejected
|
|
12
|
+
|
|
13
|
+
A rejected subscription is not recovered by subscribing again. This is
|
|
14
|
+
judged the way that the invoices subscription of ln-service judges it.
|
|
15
|
+
|
|
16
|
+
{
|
|
17
|
+
[err]: {
|
|
18
|
+
[code]: <gRPC Status Code Number>
|
|
19
|
+
[details]: <gRPC Status Details String>
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@returns
|
|
24
|
+
{
|
|
25
|
+
[error]: <Subscription Rejected Error Array>
|
|
26
|
+
}
|
|
27
|
+
*/
|
|
28
|
+
module.exports = ({err}) => {
|
|
29
|
+
// Exit early when there is no error to derive a rejection from
|
|
30
|
+
if (!err) {
|
|
31
|
+
return {};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// LND denies access with a gRPC status code, or with a message about a
|
|
35
|
+
// missing, unreadable, expired, locked, or insufficiently permissioned
|
|
36
|
+
// macaroon
|
|
37
|
+
const isDeniedCode = accessDeniedCodes.includes(err.code);
|
|
38
|
+
const isDeniedMessage = deniedMessages.some(n => n.test(err.details));
|
|
39
|
+
|
|
40
|
+
if (isDeniedCode || isDeniedMessage) {
|
|
41
|
+
return {error: [403, 'PermissionDeniedToSubscribeToMessages', {err}]};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (rejectionCodes.includes(err.code)) {
|
|
45
|
+
return {error: [503, 'SubscriptionToMessagesRejected', {err}]};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return {};
|
|
49
|
+
};
|
|
@@ -2,7 +2,6 @@ const asyncAuto = require('async/auto');
|
|
|
2
2
|
const asyncReduce = require('async/reduce');
|
|
3
3
|
const {createSignedInvoiceRequest} = require('invoices');
|
|
4
4
|
const {getNetwork} = require('ln-sync');
|
|
5
|
-
const {getPeers} = require('ln-service');
|
|
6
5
|
const {getPublicKey} = require('ln-service');
|
|
7
6
|
const {pointFromScalar} = require('tiny-secp256k1');
|
|
8
7
|
const {returnResult} = require('asyncjs-util');
|
|
@@ -30,7 +29,11 @@ const typeSchnorr = 'schnorr';
|
|
|
30
29
|
The request is sent over each path of the offer in turn, waiting `timeout`
|
|
31
30
|
for each, until a path gives back an invoice, or the recipient replies
|
|
32
31
|
with an invoice error, which is returned. An offer without paths has the
|
|
33
|
-
request sent to its issuer id.
|
|
32
|
+
request sent to its issuer id. The reply path of a request goes back along
|
|
33
|
+
its route, so that the recipient can reach it. The route to the start of a
|
|
34
|
+
path is found over the public graph and the channels of the payer, so a
|
|
35
|
+
payer that is only connected through a peer that is itself private is not
|
|
36
|
+
supported.
|
|
34
37
|
|
|
35
38
|
The request is signed by the `payer_id`, a new key that LND derives in key
|
|
36
39
|
family 808 at `payer_key_index`, only once the request is known to be one
|
|
@@ -99,9 +102,6 @@ module.exports = (args, cbk) => {
|
|
|
99
102
|
// Get the network of the node to pay the offer on
|
|
100
103
|
getNetwork: ['validate', ({}, cbk) => getNetwork({lnd: args.lnd}, cbk)],
|
|
101
104
|
|
|
102
|
-
// Get the connected peers to start the reply path at one of them
|
|
103
|
-
getPeers: ['validate', ({}, cbk) => getPeers({lnd: args.lnd}, cbk)],
|
|
104
|
-
|
|
105
105
|
// Make the request with a placeholder payer id, to check that it can be
|
|
106
106
|
// made before a payer key is derived for it
|
|
107
107
|
checkRequest: ['getNetwork', ({getNetwork}, cbk) => {
|
|
@@ -119,7 +119,7 @@ module.exports = (args, cbk) => {
|
|
|
119
119
|
}
|
|
120
120
|
}],
|
|
121
121
|
|
|
122
|
-
// Find the introduction node of each offer path
|
|
122
|
+
// Find the introduction node of each offer path to send the request to
|
|
123
123
|
getPaths: ['checkRequest', ({checkRequest}, cbk) => {
|
|
124
124
|
return resolvePathIntroductions({
|
|
125
125
|
lnd: args.lnd,
|
|
@@ -191,19 +191,13 @@ module.exports = (args, cbk) => {
|
|
|
191
191
|
}],
|
|
192
192
|
|
|
193
193
|
// Send the request over each path in turn until the recipient answers
|
|
194
|
-
getReplies: [
|
|
195
|
-
'getPeers',
|
|
196
|
-
'offerPaths',
|
|
197
|
-
'request',
|
|
198
|
-
({getPeers, offerPaths, request}, cbk) =>
|
|
199
|
-
{
|
|
194
|
+
getReplies: ['offerPaths', 'request', ({offerPaths, request}, cbk) => {
|
|
200
195
|
return asyncReduce(offerPaths, [], (replies, path, cbk) => {
|
|
201
196
|
return requestOfferInvoice({
|
|
202
197
|
path,
|
|
203
198
|
replies,
|
|
204
199
|
request,
|
|
205
200
|
lnd: args.lnd,
|
|
206
|
-
peers: getPeers.peers,
|
|
207
201
|
timeout: args.timeout || defaultTimeoutMs,
|
|
208
202
|
},
|
|
209
203
|
cbk);
|
|
@@ -3,7 +3,6 @@ const asyncReflect = require('async/reflect');
|
|
|
3
3
|
const {returnResult} = require('asyncjs-util');
|
|
4
4
|
|
|
5
5
|
const checkOfferInvoiceReply = require('./check_offer_invoice_reply');
|
|
6
|
-
const replyIntroduction = require('./../network/reply_introduction');
|
|
7
6
|
const {requestNetworkReply} = require('./../network');
|
|
8
7
|
const {typeInvoice} = require('./constants');
|
|
9
8
|
const {typeInvoiceError} = require('./constants');
|
|
@@ -20,6 +19,8 @@ const isAnswered = replies => replies.some(n => !!n.error || !!n.invoice);
|
|
|
20
19
|
A path that gives no reply, or a reply that is not a good invoice, is added
|
|
21
20
|
as a failure, so that the request can be sent over the next path.
|
|
22
21
|
|
|
22
|
+
The reply path goes back along the route of the request.
|
|
23
|
+
|
|
23
24
|
{
|
|
24
25
|
lnd: <Authenticated LND API Object>
|
|
25
26
|
path: {
|
|
@@ -30,12 +31,6 @@ const isAnswered = replies => replies.some(n => !!n.error || !!n.invoice);
|
|
|
30
31
|
introduction_node: <Introduction Node Public Key Hex String>
|
|
31
32
|
key: <First Hop Path Key Public Key Hex String>
|
|
32
33
|
}
|
|
33
|
-
peers: [{
|
|
34
|
-
features: [{
|
|
35
|
-
bit: <BOLT 09 Feature Bit Number>
|
|
36
|
-
}]
|
|
37
|
-
public_key: <Peer Public Key Hex String>
|
|
38
|
-
}]
|
|
39
34
|
replies: [<Reply Object>]
|
|
40
35
|
request: {
|
|
41
36
|
encoded: <Signed Invoice Request TLV Stream Hex String>
|
|
@@ -77,10 +72,6 @@ module.exports = (args, cbk) => {
|
|
|
77
72
|
return cbk([400, 'ExpectedOfferPathToRequestOfferInvoice']);
|
|
78
73
|
}
|
|
79
74
|
|
|
80
|
-
if (!isArray(args.peers)) {
|
|
81
|
-
return cbk([400, 'ExpectedPeersToRequestOfferInvoice']);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
75
|
if (!isArray(args.replies)) {
|
|
85
76
|
return cbk([400, 'ExpectedRepliesToRequestOfferInvoice']);
|
|
86
77
|
}
|
|
@@ -103,13 +94,7 @@ module.exports = (args, cbk) => {
|
|
|
103
94
|
return cbk();
|
|
104
95
|
}
|
|
105
96
|
|
|
106
|
-
const {introduction} = replyIntroduction({
|
|
107
|
-
path: args.path,
|
|
108
|
-
peers: args.peers,
|
|
109
|
-
});
|
|
110
|
-
|
|
111
97
|
return requestNetworkReply({
|
|
112
|
-
introduction,
|
|
113
98
|
lnd: args.lnd,
|
|
114
99
|
message: {type: typeInvoiceRequest, value: args.request.encoded},
|
|
115
100
|
path: args.path,
|
package/package.json
CHANGED
|
@@ -73,5 +73,5 @@
|
|
|
73
73
|
"integration-tests": "node test/integration",
|
|
74
74
|
"test": "node --experimental-test-coverage --test test/actions/*.js test/balanced/*.js test/client/*.js test/config/*.js test/network/*.js test/offers/*.js test/p2p/*.js test/records/*.js test/respond/*.js test/server/*.js test/service_records/*.js test/swaps/*.js test/services/*.js test/support/*.js test/trades/*.js"
|
|
75
75
|
},
|
|
76
|
-
"version": "10.
|
|
76
|
+
"version": "10.2.0"
|
|
77
77
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
const asyncAuto = require('async/auto');
|
|
2
2
|
const {getNetwork} = require('ln-sync');
|
|
3
|
-
const {getPeers} = require('ln-service');
|
|
4
3
|
const {parseInvoiceError} = require('invoices');
|
|
5
4
|
const {parseOffer} = require('invoices');
|
|
6
5
|
const {returnResult} = require('asyncjs-util');
|
|
@@ -10,10 +9,10 @@ const decodeSupportMenu = require('./decode_support_menu');
|
|
|
10
9
|
const {defaultMenuTimeoutMs} = require('./constants');
|
|
11
10
|
const isReplyTooLarge = require('./is_reply_too_large');
|
|
12
11
|
const isSupportOffer = require('./is_support_offer');
|
|
12
|
+
const {maxReplyHops} = require('./constants');
|
|
13
13
|
const menuOffers = require('./menu_offers');
|
|
14
14
|
const offerBitcoinNetwork = require('./offer_bitcoin_network');
|
|
15
15
|
const offerSuggestedAmounts = require('./offer_suggested_amounts');
|
|
16
|
-
const replyIntroduction = require('./../network/reply_introduction');
|
|
17
16
|
const {requestNetworkReply} = require('./../network');
|
|
18
17
|
const {resolvePathIntroductions} = require('./../network');
|
|
19
18
|
const {typeInvoice} = require('./constants');
|
|
@@ -34,8 +33,9 @@ const isTimeout = n => n === undefined || (isSafeInteger(n) && n > 0);
|
|
|
34
33
|
into pages. The first page is page zero, and each page says how many pages
|
|
35
34
|
there are. The request is a signed invoice request with the menu request
|
|
36
35
|
record, sent over the first path of the offer with a payer id that is only
|
|
37
|
-
used once, and the reply path
|
|
38
|
-
|
|
36
|
+
used once, and the reply path goes back along the route of the request,
|
|
37
|
+
keeping the hops nearest the payer when it has more than the eight hops
|
|
38
|
+
that a recipient answers. A reply without a page means the menu is not
|
|
39
39
|
supported, and an invoice sent back is not paid.
|
|
40
40
|
|
|
41
41
|
A page is only used when it is signed by the support offer issuer id, has
|
|
@@ -160,9 +160,6 @@ module.exports = (args, cbk) => {
|
|
|
160
160
|
// Get the network of the node to check offers are for it
|
|
161
161
|
getNetwork: ['details', ({}, cbk) => getNetwork({lnd: args.lnd}, cbk)],
|
|
162
162
|
|
|
163
|
-
// Get the connected peers to start the reply path at one of them
|
|
164
|
-
getPeers: ['details', ({}, cbk) => getPeers({lnd: args.lnd}, cbk)],
|
|
165
|
-
|
|
166
163
|
// Check the offer is for the network of the node
|
|
167
164
|
checkNetwork: ['details', 'getNetwork', ({getNetwork}, cbk) => {
|
|
168
165
|
const {network} = offerBitcoinNetwork({offer: args.offer});
|
|
@@ -174,7 +171,7 @@ module.exports = (args, cbk) => {
|
|
|
174
171
|
return cbk();
|
|
175
172
|
}],
|
|
176
173
|
|
|
177
|
-
// Find the introduction node of the first path
|
|
174
|
+
// Find the introduction node of the first path to send the request to
|
|
178
175
|
getPath: ['details', ({details}, cbk) => {
|
|
179
176
|
const [path] = details.paths;
|
|
180
177
|
|
|
@@ -196,15 +193,6 @@ module.exports = (args, cbk) => {
|
|
|
196
193
|
});
|
|
197
194
|
}],
|
|
198
195
|
|
|
199
|
-
// Select a peer that relays onion messages to start the reply path
|
|
200
|
-
introduction: ['getPath', 'getPeers', ({getPath, getPeers}, cbk) => {
|
|
201
|
-
const {peers} = getPeers;
|
|
202
|
-
|
|
203
|
-
const {introduction} = replyIntroduction({path: getPath, peers});
|
|
204
|
-
|
|
205
|
-
return cbk(null, introduction);
|
|
206
|
-
}],
|
|
207
|
-
|
|
208
196
|
// Make a signed invoice request that asks for the page
|
|
209
197
|
message: ['checkNetwork', ({}, cbk) => {
|
|
210
198
|
try {
|
|
@@ -221,16 +209,11 @@ module.exports = (args, cbk) => {
|
|
|
221
209
|
}],
|
|
222
210
|
|
|
223
211
|
// Send the request and wait for the reply
|
|
224
|
-
getReply: [
|
|
225
|
-
'getPath',
|
|
226
|
-
'introduction',
|
|
227
|
-
'message',
|
|
228
|
-
({getPath, introduction, message}, cbk) =>
|
|
229
|
-
{
|
|
212
|
+
getReply: ['getPath', 'message', ({getPath, message}, cbk) => {
|
|
230
213
|
return requestNetworkReply({
|
|
231
|
-
introduction,
|
|
232
214
|
message,
|
|
233
215
|
lnd: args.lnd,
|
|
216
|
+
max_reply_hops: maxReplyHops,
|
|
234
217
|
path: getPath,
|
|
235
218
|
timeout: args.timeout || defaultMenuTimeoutMs,
|
|
236
219
|
types: [typeInvoice, typeInvoiceError],
|
|
@@ -74,6 +74,8 @@ const typeRequestAmount = '82';
|
|
|
74
74
|
checked to be a support offer of a supported version, and its signing key
|
|
75
75
|
is checked to be the offer issuer id. When a check fails, or the related
|
|
76
76
|
offers cannot be made into menus, an error is emitted and the service ends.
|
|
77
|
+
When the checks pass, the `start` listener is given the offer, so that a
|
|
78
|
+
service resumed from its record can show the offer again.
|
|
77
79
|
|
|
78
80
|
Requests that did not arrive on an offer path are ignored. Invalid requests
|
|
79
81
|
on an offer path are sent an invoice error. A request that is sent again is
|
|
@@ -115,6 +117,11 @@ const typeRequestAmount = '82';
|
|
|
115
117
|
stops the handling of requests that are being handled, so nothing more is
|
|
116
118
|
sent.
|
|
117
119
|
|
|
120
|
+
The service gets through a disconnection from LND, like LND restarting:
|
|
121
|
+
the error is emitted, onion messages are subscribed to again after a
|
|
122
|
+
delay, and invoices paid in the meantime are still reported. The service
|
|
123
|
+
ends when LND rejects a subscription, or does not have onion messages.
|
|
124
|
+
|
|
118
125
|
{
|
|
119
126
|
lnd: <Authenticated LND API Object>
|
|
120
127
|
record: <Support Service Record Hex String>
|
|
@@ -140,10 +147,17 @@ const typeRequestAmount = '82';
|
|
|
140
147
|
invoice: <Add an Invoice Sent Listener Function>
|
|
141
148
|
invoice_error: <Add an Invoice Error Sent Listener Function>
|
|
142
149
|
menu: <Add a Menu Sent Listener Function>
|
|
150
|
+
start: <Add a Service Started Listener Function>
|
|
143
151
|
stop: <Stop Listening Function> ({}) => {};
|
|
144
152
|
supporter: <Add a Supporter Paid Listener Function>
|
|
145
153
|
}
|
|
146
154
|
|
|
155
|
+
start:
|
|
156
|
+
@returns via cbk
|
|
157
|
+
{
|
|
158
|
+
offer: <BOLT 12 Support Offer String>
|
|
159
|
+
}
|
|
160
|
+
|
|
147
161
|
supporter:
|
|
148
162
|
@returns via cbk
|
|
149
163
|
{
|
|
@@ -269,6 +283,7 @@ module.exports = args => {
|
|
|
269
283
|
invoice: () => {},
|
|
270
284
|
invoice_error: () => {},
|
|
271
285
|
menu: () => {},
|
|
286
|
+
start: () => {},
|
|
272
287
|
};
|
|
273
288
|
const waiting = [];
|
|
274
289
|
|
|
@@ -284,6 +299,11 @@ module.exports = args => {
|
|
|
284
299
|
// Tell a listener about an event apart from the code that found it, so
|
|
285
300
|
// that a listener that throws cannot stop the service or crash it
|
|
286
301
|
const emit = (event, details) => process.nextTick(() => {
|
|
302
|
+
// Exit early when the service stopped before it could say it started
|
|
303
|
+
if (event === 'start' && isStopped) {
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
|
|
287
307
|
try {
|
|
288
308
|
return service[event](details);
|
|
289
309
|
} catch (err) {
|
|
@@ -512,14 +532,26 @@ module.exports = args => {
|
|
|
512
532
|
(err, res) => {
|
|
513
533
|
isChecked = true;
|
|
514
534
|
|
|
535
|
+
// Exit early when the service stopped or failed while the record was
|
|
536
|
+
// checked, so that it does not start
|
|
537
|
+
if (isStopped) {
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
|
|
515
541
|
if (!!err) {
|
|
516
|
-
failed(err);
|
|
517
|
-
} else {
|
|
518
|
-
state = res.state;
|
|
542
|
+
return failed(err);
|
|
519
543
|
}
|
|
520
544
|
|
|
521
|
-
|
|
522
|
-
|
|
545
|
+
state = res.state;
|
|
546
|
+
|
|
547
|
+
// The service has started, and the offer can be shown again
|
|
548
|
+
emit('start', {offer: state.offer});
|
|
549
|
+
|
|
550
|
+
// Requests that arrived while the record was checked are handled now,
|
|
551
|
+
// after the start listener is told
|
|
552
|
+
return process.nextTick(() => {
|
|
553
|
+
return waiting.splice(0).forEach(handle => handle());
|
|
554
|
+
});
|
|
523
555
|
}));
|
|
524
556
|
|
|
525
557
|
invoicesSub.on('error', err => failed(err));
|
|
@@ -1020,6 +1052,7 @@ module.exports = args => {
|
|
|
1020
1052
|
invoice: cbk => service.invoice = cbk,
|
|
1021
1053
|
invoice_error: cbk => service.invoice_error = cbk,
|
|
1022
1054
|
menu: cbk => service.menu = cbk,
|
|
1055
|
+
start: cbk => service.start = cbk,
|
|
1023
1056
|
stop: ({}) => {
|
|
1024
1057
|
isStopped = true;
|
|
1025
1058
|
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
const {randomInt} = require('node:crypto');
|
|
2
|
-
|
|
3
|
-
const onionMessagesBits = [38, 39];
|
|
4
|
-
|
|
5
|
-
/** Select a connected peer to start a reply path at
|
|
6
|
-
|
|
7
|
-
The peer has to relay onion messages, and is not the start of the path that
|
|
8
|
-
the request is sent over. A random peer is selected from those that can be
|
|
9
|
-
used, and there is no peer when none can be used.
|
|
10
|
-
|
|
11
|
-
{
|
|
12
|
-
path: {
|
|
13
|
-
[introduction_node]: <Request Path Introduction Public Key Hex String>
|
|
14
|
-
}
|
|
15
|
-
peers: [{
|
|
16
|
-
features: [{
|
|
17
|
-
bit: <BOLT 09 Feature Bit Number>
|
|
18
|
-
}]
|
|
19
|
-
public_key: <Peer Public Key Hex String>
|
|
20
|
-
}]
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
@returns
|
|
24
|
-
{
|
|
25
|
-
[introduction]: <Reply Path Introduction Peer Public Key Hex String>
|
|
26
|
-
}
|
|
27
|
-
*/
|
|
28
|
-
module.exports = ({path, peers}) => {
|
|
29
|
-
const relays = peers.filter(peer => {
|
|
30
|
-
// The start of the request path is not used for the reply path
|
|
31
|
-
if (peer.public_key === path.introduction_node) {
|
|
32
|
-
return false;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
return !!peer.features.find(n => onionMessagesBits.includes(n.bit));
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
// Exit early when there is no peer to start the reply path at
|
|
39
|
-
if (!relays.length) {
|
|
40
|
-
return {};
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return {introduction: relays[randomInt(relays.length)].public_key};
|
|
44
|
-
};
|