paid-services 10.1.0 → 10.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -10
- package/network/constants.json +18 -0
- package/network/get_message_hops.js +64 -10
- package/network/get_path_after_self.js +222 -0
- package/network/get_path_outbound.js +104 -14
- package/network/make_network_request.js +2 -1
- package/network/request_network_reply.js +54 -16
- package/network/return_network_response.js +33 -17
- package/network/service_network_messages.js +106 -27
- 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,27 @@
|
|
|
1
1
|
# Versions
|
|
2
2
|
|
|
3
|
+
## Version 10.3.0
|
|
4
|
+
|
|
5
|
+
- `getOfferInvoice`: Add support for offer paths that start at the payer
|
|
6
|
+
- `serviceSupportOffer`: Add support for reply paths that start at the node
|
|
7
|
+
|
|
8
|
+
## Version 10.2.0
|
|
9
|
+
|
|
10
|
+
- `serviceSupportOffer`: Add `start` event with the support offer
|
|
11
|
+
|
|
3
12
|
## Version 10.1.0
|
|
4
13
|
|
|
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
|
|
14
|
+
- `createSupportOffer`: Add `introduction` to start the created offer path
|
|
10
15
|
|
|
11
16
|
## Version 10.0.0
|
|
12
17
|
|
|
13
18
|
- `getOfferInvoice`: Add `features` to show the features of the invoice
|
|
14
19
|
- `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
20
|
|
|
18
21
|
### Breaking Changes
|
|
19
22
|
|
|
20
23
|
- `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`
|
|
24
|
+
- `getOfferInvoice`: Seed derived `payer_key_index` replaces `payer_secret`
|
|
24
25
|
|
|
25
26
|
## Version 9.3.0
|
|
26
27
|
|
|
@@ -0,0 +1,18 @@
|
|
|
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
|
+
"knownHopRecordTypes": ["1", "2", "4", "6", "8", "10", "12", "14"],
|
|
17
|
+
"typePathId": "6"
|
|
18
|
+
}
|
|
@@ -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) => {
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
const asyncAuto = require('async/auto');
|
|
2
|
+
const {decryptBlindedPath} = require('bolt04');
|
|
3
|
+
const {diffieHellmanComputeSecret} = require('ln-service');
|
|
4
|
+
const {getChannel} = require('ln-service');
|
|
5
|
+
const {getChannels} = require('ln-service');
|
|
6
|
+
const {returnResult} = require('asyncjs-util');
|
|
7
|
+
|
|
8
|
+
const {knownHopRecordTypes} = require('./constants');
|
|
9
|
+
const {typePathId} = require('./constants');
|
|
10
|
+
|
|
11
|
+
const hasPathId = records => records.some(n => n.type === typePathId);
|
|
12
|
+
const hopsAfterFirst = hops => hops.slice(1);
|
|
13
|
+
const idsOf = channel => [channel.id].concat(channel.other_ids);
|
|
14
|
+
const {isArray} = Array;
|
|
15
|
+
const isEven = type => !(BigInt(type) % BigInt(2));
|
|
16
|
+
const isKnown = record => knownHopRecordTypes.includes(record.type);
|
|
17
|
+
const isPublicKey = n => typeof n === 'string' && /^[0-9A-F]{66}$/i.test(n);
|
|
18
|
+
const isUnknownEven = record => !isKnown(record) && isEven(record.type);
|
|
19
|
+
|
|
20
|
+
/** Get the blinded path after self, for a blinded path that starts at self
|
|
21
|
+
|
|
22
|
+
Self reads its own hop as an introduction node would. The path goes on from
|
|
23
|
+
the next node, which the hop names, or which is the peer at the other end
|
|
24
|
+
of the channel that the hop names.
|
|
25
|
+
|
|
26
|
+
As BOLT 4 asks of a node that forwards, the hop of self is not used when it
|
|
27
|
+
has an unknown even record, when it has a path id, or when it allows
|
|
28
|
+
features, since no onion message features are defined.
|
|
29
|
+
|
|
30
|
+
{
|
|
31
|
+
hops: [{
|
|
32
|
+
encrypted_data: <Encrypted Recipient Data Hex String>
|
|
33
|
+
relay_key: <Blinded Relaying Node Public Key Hex String>
|
|
34
|
+
}]
|
|
35
|
+
identity: <Node Identity Public Key Hex String>
|
|
36
|
+
key: <First Hop Path Key Public Key Hex String>
|
|
37
|
+
lnd: <Authenticated LND API Object>
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@returns via cbk or Promise
|
|
41
|
+
{
|
|
42
|
+
hops: [{
|
|
43
|
+
encrypted_data: <Encrypted Recipient Data Hex String>
|
|
44
|
+
relay_key: <Blinded Relaying Node Public Key Hex String>
|
|
45
|
+
}]
|
|
46
|
+
introduction_node: <Next Node Public Key Hex String>
|
|
47
|
+
key: <Next Hop Path Key Public Key Hex String>
|
|
48
|
+
}
|
|
49
|
+
*/
|
|
50
|
+
module.exports = (args, cbk) => {
|
|
51
|
+
return new Promise((resolve, reject) => {
|
|
52
|
+
return asyncAuto({
|
|
53
|
+
// Check arguments
|
|
54
|
+
validate: cbk => {
|
|
55
|
+
if (!isArray(args.hops) || !args.hops.length) {
|
|
56
|
+
return cbk([400, 'ExpectedBlindedPathHopsToGetPathAfterSelf']);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!isPublicKey(args.identity)) {
|
|
60
|
+
return cbk([400, 'ExpectedIdentityPublicKeyToGetPathAfterSelf']);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!args.key) {
|
|
64
|
+
return cbk([400, 'ExpectedPathKeyToGetPathAfterSelf']);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!args.lnd) {
|
|
68
|
+
return cbk([400, 'ExpectedLndToGetPathAfterSelf']);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return cbk();
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
// Compute the shared secret of the path key and the node identity key
|
|
75
|
+
getSecret: ['validate', ({}, cbk) => {
|
|
76
|
+
return diffieHellmanComputeSecret({
|
|
77
|
+
lnd: args.lnd,
|
|
78
|
+
partner_public_key: args.key,
|
|
79
|
+
},
|
|
80
|
+
cbk);
|
|
81
|
+
}],
|
|
82
|
+
|
|
83
|
+
// Decrypt the data of the hop of self to read where the path goes next
|
|
84
|
+
decrypt: ['getSecret', ({getSecret}, cbk) => {
|
|
85
|
+
const [own] = args.hops;
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
return cbk(null, decryptBlindedPath({
|
|
89
|
+
encrypted: own.encrypted_data,
|
|
90
|
+
key: args.key,
|
|
91
|
+
secret: getSecret.secret,
|
|
92
|
+
}));
|
|
93
|
+
} catch (err) {
|
|
94
|
+
return cbk([503, 'FailedToDecryptOwnBlindedPathHop', {err}]);
|
|
95
|
+
}
|
|
96
|
+
}],
|
|
97
|
+
|
|
98
|
+
// Check the hop of self can be followed, as a forwarding node would
|
|
99
|
+
check: ['decrypt', ({decrypt}, cbk) => {
|
|
100
|
+
// An unknown even record cannot be ignored, so the hop cannot be used
|
|
101
|
+
if (decrypt.records.some(isUnknownEven)) {
|
|
102
|
+
return cbk([
|
|
103
|
+
400,
|
|
104
|
+
'UnexpectedUnknownRequiredRecordInOwnBlindedPathHop',
|
|
105
|
+
]);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// No features are defined for onion messages, so a feature bit in
|
|
109
|
+
// the allowed features is an unknown feature, and the hop is not used
|
|
110
|
+
if (!!decrypt.features.length) {
|
|
111
|
+
return cbk([400, 'UnsupportedFeatureInOwnBlindedPathHop']);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// A hop with no next node or channel is the final hop of the path
|
|
115
|
+
if (!decrypt.next_node_id && !decrypt.next_channel_id) {
|
|
116
|
+
return cbk([400, 'UnexpectedBlindedPathToSelf']);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// A hop that goes on to a next node is not one with a path id record,
|
|
120
|
+
// which is present even when the path id is empty
|
|
121
|
+
if (hasPathId(decrypt.records)) {
|
|
122
|
+
return cbk([400, 'UnexpectedPathIdInOwnBlindedPathHop']);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return cbk();
|
|
126
|
+
}],
|
|
127
|
+
|
|
128
|
+
// The hops after self, which the path goes on over
|
|
129
|
+
hops: ['check', ({}, cbk) => {
|
|
130
|
+
const hops = hopsAfterFirst(args.hops);
|
|
131
|
+
|
|
132
|
+
// The next node has a hop of its own to send to
|
|
133
|
+
if (!hops.length) {
|
|
134
|
+
return cbk([400, 'ExpectedHopsAfterSelfInBlindedPath']);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return cbk(null, hops);
|
|
138
|
+
}],
|
|
139
|
+
|
|
140
|
+
// Get the channels of self when the next node is named by a channel
|
|
141
|
+
getChannels: ['decrypt', 'hops', ({decrypt}, cbk) => {
|
|
142
|
+
// Exit early when the hop names the next node
|
|
143
|
+
if (!!decrypt.next_node_id) {
|
|
144
|
+
return cbk();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return getChannels({lnd: args.lnd}, cbk);
|
|
148
|
+
}],
|
|
149
|
+
|
|
150
|
+
// Find the next channel among the channels of self
|
|
151
|
+
channel: ['decrypt', 'getChannels', ({decrypt, getChannels}, cbk) => {
|
|
152
|
+
// Exit early when the hop names the next node
|
|
153
|
+
if (!getChannels) {
|
|
154
|
+
return cbk();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const id = decrypt.next_channel_id;
|
|
158
|
+
|
|
159
|
+
// A private channel can be named by one of its aliases
|
|
160
|
+
return cbk(null, getChannels.channels.find(n => idsOf(n).includes(id)));
|
|
161
|
+
}],
|
|
162
|
+
|
|
163
|
+
// Look for the next channel in the graph when self does not list it
|
|
164
|
+
getChannel: ['channel', 'decrypt', ({channel, decrypt}, cbk) => {
|
|
165
|
+
// Exit early when the next node is already known
|
|
166
|
+
if (!!decrypt.next_node_id || !!channel) {
|
|
167
|
+
return cbk();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return getChannel({
|
|
171
|
+
id: decrypt.next_channel_id,
|
|
172
|
+
lnd: args.lnd,
|
|
173
|
+
},
|
|
174
|
+
(err, res) => {
|
|
175
|
+
// Exit early when the channel is not in the graph
|
|
176
|
+
if (!!err) {
|
|
177
|
+
return cbk([503, 'FailedToFindNextPeerInBlindedPath']);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return cbk(null, res);
|
|
181
|
+
});
|
|
182
|
+
}],
|
|
183
|
+
|
|
184
|
+
// The next node is the one that the hop names, or is across the channel
|
|
185
|
+
next: [
|
|
186
|
+
'channel',
|
|
187
|
+
'decrypt',
|
|
188
|
+
'getChannel',
|
|
189
|
+
({channel, decrypt, getChannel}, cbk) =>
|
|
190
|
+
{
|
|
191
|
+
// Exit early when the hop names the next node
|
|
192
|
+
if (!!decrypt.next_node_id) {
|
|
193
|
+
return cbk(null, decrypt.next_node_id);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Exit early when the channel is a channel of self
|
|
197
|
+
if (!!channel) {
|
|
198
|
+
return cbk(null, channel.partner_public_key);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const keys = getChannel.policies.map(n => n.public_key);
|
|
202
|
+
|
|
203
|
+
// The channel in the graph has to be one that self forwards over
|
|
204
|
+
if (!keys.includes(args.identity)) {
|
|
205
|
+
return cbk([503, 'FailedToFindNextPeerInBlindedPath']);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return cbk(null, keys.find(n => n !== args.identity));
|
|
209
|
+
}],
|
|
210
|
+
|
|
211
|
+
// The path goes on from the next node, with the next hop path key
|
|
212
|
+
path: ['decrypt', 'hops', 'next', ({decrypt, hops, next}, cbk) => {
|
|
213
|
+
return cbk(null, {
|
|
214
|
+
hops,
|
|
215
|
+
introduction_node: next,
|
|
216
|
+
key: decrypt.next_path_key,
|
|
217
|
+
});
|
|
218
|
+
}],
|
|
219
|
+
},
|
|
220
|
+
returnResult({reject, resolve, of: 'path'}, cbk));
|
|
221
|
+
});
|
|
222
|
+
};
|
|
@@ -1,80 +1,170 @@
|
|
|
1
1
|
const asyncAuto = require('async/auto');
|
|
2
|
+
const asyncWhilst = require('async/whilst');
|
|
2
3
|
const {getChannel} = require('ln-service');
|
|
4
|
+
const {getIdentity} = require('ln-service');
|
|
3
5
|
const {returnResult} = require('asyncjs-util');
|
|
4
6
|
|
|
5
7
|
const getMessageHops = require('./get_message_hops');
|
|
8
|
+
const getPathAfterSelf = require('./get_path_after_self');
|
|
6
9
|
const pathIntroduction = require('./path_introduction');
|
|
7
10
|
|
|
8
11
|
const edgeChannel = edge => edge.split('x').slice(0, -1).join('x');
|
|
12
|
+
const {isArray} = Array;
|
|
13
|
+
const isPublicKey = n => typeof n === 'string' && /^[0-9A-F]{66}$/i.test(n);
|
|
9
14
|
|
|
10
|
-
/** Get the outbound hops to reach the introduction node of a blinded path
|
|
15
|
+
/** Get the outbound hops to reach the introduction node of a blinded path,
|
|
16
|
+
with the hops and the path key to send over
|
|
11
17
|
|
|
12
18
|
When the introduction node is not a connected peer, the message is relayed
|
|
13
19
|
along a payment route through nodes that relay onion messages
|
|
14
20
|
|
|
21
|
+
A path that starts at self is sent from the next node of the path: self
|
|
22
|
+
reads its own hops, as an introduction node would, and sends over the hops
|
|
23
|
+
after them. A path that does not start at self is sent over as it is.
|
|
24
|
+
|
|
15
25
|
{
|
|
26
|
+
[is_expecting_reply]: <Reply Comes Back Along the Same Nodes Bool>
|
|
16
27
|
lnd: <Authenticated LND API Object>
|
|
17
28
|
path: {
|
|
29
|
+
hops: [{
|
|
30
|
+
encrypted_data: <Encrypted Recipient Data Hex String>
|
|
31
|
+
relay_key: <Blinded Relaying Node Public Key Hex String>
|
|
32
|
+
}]
|
|
18
33
|
[introduction_edge]: <Introduction Node Edge Format Channel Id String>
|
|
19
34
|
[introduction_node]: <Introduction Node Public Key Hex String>
|
|
35
|
+
key: <First Hop Path Key Public Key Hex String>
|
|
20
36
|
}
|
|
21
37
|
}
|
|
22
38
|
|
|
23
39
|
@returns via cbk or Promise
|
|
24
40
|
{
|
|
41
|
+
hops: [{
|
|
42
|
+
encrypted_data: <Encrypted Recipient Data Hex String>
|
|
43
|
+
relay_key: <Blinded Relaying Node Public Key Hex String>
|
|
44
|
+
}]
|
|
45
|
+
key: <First Hop Path Key Public Key Hex String>
|
|
25
46
|
outbound: [<Relaying Node Public Key Hex String>]
|
|
26
47
|
}
|
|
27
48
|
*/
|
|
28
|
-
module.exports = (
|
|
49
|
+
module.exports = (args, cbk) => {
|
|
29
50
|
return new Promise((resolve, reject) => {
|
|
30
51
|
return asyncAuto({
|
|
31
52
|
// Check arguments
|
|
32
53
|
validate: cbk => {
|
|
33
|
-
if (!lnd) {
|
|
54
|
+
if (!args.lnd) {
|
|
34
55
|
return cbk([400, 'ExpectedLndToGetPathOutbound']);
|
|
35
56
|
}
|
|
36
57
|
|
|
37
|
-
if (!path) {
|
|
58
|
+
if (!args.path) {
|
|
38
59
|
return cbk([400, 'ExpectedBlindedPathToGetPathOutbound']);
|
|
39
60
|
}
|
|
40
61
|
|
|
62
|
+
if (!isArray(args.path.hops) || !args.path.hops.length) {
|
|
63
|
+
return cbk([400, 'ExpectedBlindedPathHopsToGetPathOutbound']);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// A node introduction is compared to self as a public key
|
|
67
|
+
if (!!args.path.introduction_node) {
|
|
68
|
+
if (!isPublicKey(args.path.introduction_node)) {
|
|
69
|
+
return cbk([400, 'ExpectedIntroductionNodeKeyToGetPathOutbound']);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!args.path.key) {
|
|
74
|
+
return cbk([400, 'ExpectedBlindedPathKeyToGetPathOutbound']);
|
|
75
|
+
}
|
|
76
|
+
|
|
41
77
|
return cbk();
|
|
42
78
|
},
|
|
43
79
|
|
|
44
80
|
// Get the channel of the introduction edge
|
|
45
81
|
getChannel: ['validate', ({}, cbk) => {
|
|
46
82
|
// Exit early when the introduction is a node and not an edge
|
|
47
|
-
if (!path.introduction_edge) {
|
|
83
|
+
if (!args.path.introduction_edge) {
|
|
48
84
|
return cbk();
|
|
49
85
|
}
|
|
50
86
|
|
|
51
|
-
const id = edgeChannel(path.introduction_edge);
|
|
87
|
+
const id = edgeChannel(args.path.introduction_edge);
|
|
52
88
|
|
|
53
|
-
return getChannel({id, lnd}, cbk);
|
|
89
|
+
return getChannel({id, lnd: args.lnd}, cbk);
|
|
90
|
+
}],
|
|
91
|
+
|
|
92
|
+
// Get the identity key of self, which the path can start at
|
|
93
|
+
getIdentity: ['validate', ({}, cbk) => {
|
|
94
|
+
return getIdentity({lnd: args.lnd}, cbk);
|
|
54
95
|
}],
|
|
55
96
|
|
|
56
97
|
// Determine the introduction node of the path
|
|
57
98
|
introduction: ['getChannel', ({getChannel}, cbk) => {
|
|
58
99
|
try {
|
|
59
100
|
const {introduction} = pathIntroduction({
|
|
60
|
-
path,
|
|
61
101
|
channel: getChannel,
|
|
102
|
+
path: args.path,
|
|
62
103
|
});
|
|
63
104
|
|
|
64
|
-
|
|
105
|
+
// Node keys are compared in lowercase, as LND gives them
|
|
106
|
+
return cbk(null, introduction.toLowerCase());
|
|
65
107
|
} catch (err) {
|
|
66
108
|
return cbk([400, err.message]);
|
|
67
109
|
}
|
|
68
110
|
}],
|
|
69
111
|
|
|
112
|
+
// Step past the hops of self while the path starts at self
|
|
113
|
+
stepped: [
|
|
114
|
+
'getIdentity',
|
|
115
|
+
'introduction',
|
|
116
|
+
({getIdentity, introduction}, cbk) =>
|
|
117
|
+
{
|
|
118
|
+
const identity = getIdentity.public_key;
|
|
119
|
+
|
|
120
|
+
let path = {
|
|
121
|
+
hops: args.path.hops,
|
|
122
|
+
introduction_node: introduction,
|
|
123
|
+
key: args.path.key,
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
// Self reads its own hop as an introduction node would, and the path
|
|
127
|
+
// goes on from the next node, which can be self again
|
|
128
|
+
return asyncWhilst(
|
|
129
|
+
cbk => cbk(null, path.introduction_node === identity),
|
|
130
|
+
cbk => {
|
|
131
|
+
return getPathAfterSelf({
|
|
132
|
+
identity,
|
|
133
|
+
hops: path.hops,
|
|
134
|
+
key: path.key,
|
|
135
|
+
lnd: args.lnd,
|
|
136
|
+
},
|
|
137
|
+
(err, res) => {
|
|
138
|
+
if (!!err) {
|
|
139
|
+
return cbk(err);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
path = res;
|
|
143
|
+
|
|
144
|
+
return cbk();
|
|
145
|
+
});
|
|
146
|
+
},
|
|
147
|
+
err => !!err ? cbk(err) : cbk(null, path)
|
|
148
|
+
);
|
|
149
|
+
}],
|
|
150
|
+
|
|
70
151
|
// Get the hops to relay the message to the introduction node
|
|
71
|
-
getHops: ['
|
|
72
|
-
return getMessageHops({
|
|
152
|
+
getHops: ['stepped', ({stepped}, cbk) => {
|
|
153
|
+
return getMessageHops({
|
|
154
|
+
destination: stepped.introduction_node,
|
|
155
|
+
is_expecting_reply: args.is_expecting_reply,
|
|
156
|
+
lnd: args.lnd,
|
|
157
|
+
},
|
|
158
|
+
cbk);
|
|
73
159
|
}],
|
|
74
160
|
|
|
75
|
-
// The outbound hops end at the introduction node
|
|
76
|
-
outbound: ['getHops', ({getHops}, cbk) => {
|
|
77
|
-
return cbk(null, {
|
|
161
|
+
// The outbound hops end at the introduction node of the path to send
|
|
162
|
+
outbound: ['getHops', 'stepped', ({getHops, stepped}, cbk) => {
|
|
163
|
+
return cbk(null, {
|
|
164
|
+
hops: stepped.hops,
|
|
165
|
+
key: stepped.key,
|
|
166
|
+
outbound: getHops.hops,
|
|
167
|
+
});
|
|
78
168
|
}],
|
|
79
169
|
},
|
|
80
170
|
returnResult({reject, resolve, of: 'outbound'}, cbk));
|
|
@@ -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
|
|