paid-services 9.0.1 → 9.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 +11 -0
- package/groups/assemble/coordinate_group.js +3 -9
- package/groups/assemble_channel_group.js +6 -8
- package/groups/coordinate_channel_group.js +1 -7
- package/groups/create_group_channel.js +4 -8
- package/groups/fanout/assemble_fanout_group.js +3 -6
- package/groups/fanout/coordinate_fanout.js +4 -9
- package/groups/fanout/create_group_fanout.js +1 -7
- package/groups/fanout/get_fanout_funding.js +2 -6
- package/groups/fanout/join_fanout.js +2 -7
- package/groups/funding/sign_and_fund_group_channel.js +2 -6
- package/groups/join_channel_group.js +3 -11
- package/groups/messages/decode_signed_funding.js +3 -5
- package/groups/p2p/register_fanout_proposal.js +3 -9
- package/groups/p2p/register_pending_open.js +3 -8
- package/index.js +8 -0
- package/network/create_network_path.js +122 -0
- package/network/encode_network_response.js +37 -0
- package/network/get_message_hops.js +130 -0
- package/network/get_path_id.js +61 -0
- package/network/get_path_outbound.js +82 -0
- package/network/get_relaying_route.js +100 -0
- package/network/index.js +15 -0
- package/network/make_network_request.js +152 -0
- package/network/parse_network_request.js +34 -0
- package/network/path_introduction.js +65 -0
- package/network/regular_reply_bytes.js +66 -0
- package/network/relays_as_hops.js +37 -0
- package/network/reply_introduction.js +44 -0
- package/network/request_network_reply.js +239 -0
- package/network/resolve_path_introductions.js +100 -0
- package/network/return_network_response.js +115 -0
- package/network/route_relays.js +44 -0
- package/network/service_network_messages.js +278 -0
- package/network/service_network_requests.js +180 -0
- package/offers/check_offer_invoice_reply.js +82 -0
- package/offers/constants.json +5 -0
- package/offers/get_offer_invoice.js +194 -0
- package/offers/index.js +5 -0
- package/offers/invoice_request_for_offer.js +147 -0
- package/offers/offer_invoice_failure.js +32 -0
- package/offers/pay_offer.js +101 -0
- package/offers/pay_offer_invoice.js +138 -0
- package/offers/random_secret.js +24 -0
- package/p2p/service_peer_requests.js +0 -3
- package/package.json +13 -8
- package/service_records/add_service_label.js +55 -0
- package/service_records/constants.json +21 -0
- package/service_records/decode_records.js +49 -0
- package/service_records/decode_service_label.js +79 -0
- package/service_records/decode_service_record.js +106 -0
- package/service_records/encode_service_label.js +71 -0
- package/service_records/encode_service_record.js +116 -0
- package/service_records/get_record_key.js +84 -0
- package/service_records/hex_as_list.js +60 -0
- package/service_records/hex_as_version.js +27 -0
- package/service_records/index.js +37 -0
- package/service_records/known_types.js +27 -0
- package/service_records/list_as_hex.js +34 -0
- package/service_records/number_as_truncated.js +39 -0
- package/service_records/open_sealed_data.js +50 -0
- package/service_records/read_record_key_index.js +95 -0
- package/service_records/read_service_label.js +62 -0
- package/service_records/record_key_for_secret.js +33 -0
- package/service_records/record_point.js +24 -0
- package/service_records/seal_key_for_salt.js +41 -0
- package/service_records/truncated_as_number.js +33 -0
- package/service_records/version_as_hex.js +28 -0
- package/service_records/without_record.js +43 -0
- package/support/add_offer_records.js +49 -0
- package/support/amounts_as_hex.js +35 -0
- package/support/bip353_name_as_hex.js +36 -0
- package/support/bitcoin_chains.json +7 -0
- package/support/call_or_throw.js +23 -0
- package/support/constants.json +72 -0
- package/support/create_menu_request.js +91 -0
- package/support/create_support_offer.js +365 -0
- package/support/decode_menu_entry.js +129 -0
- package/support/decode_safe_number.js +28 -0
- package/support/decode_suggested_amounts.js +63 -0
- package/support/decode_support_menu.js +168 -0
- package/support/decode_support_record.js +123 -0
- package/support/encode_menu_entry.js +128 -0
- package/support/encode_payer_offer.js +104 -0
- package/support/encode_suggested_amounts.js +56 -0
- package/support/encode_support_menu.js +132 -0
- package/support/encode_support_record.js +104 -0
- package/support/erroneous_request_field.js +27 -0
- package/support/erroneous_request_fields.json +48 -0
- package/support/get_support_menu.js +357 -0
- package/support/has_unknown_even_record.js +23 -0
- package/support/hex_as_amounts.js +34 -0
- package/support/hex_as_bip353_name.js +55 -0
- package/support/index.js +15 -0
- package/support/invoice_for_request.js +116 -0
- package/support/invoice_paths_to_fit.js +80 -0
- package/support/is_reply_too_large.js +24 -0
- package/support/is_support_offer.js +64 -0
- package/support/known_record_types.js +37 -0
- package/support/length_without_record.js +25 -0
- package/support/menu_id_for_pages.js +49 -0
- package/support/menu_offers.js +173 -0
- package/support/menu_pages.js +81 -0
- package/support/menu_signature_preimage.js +74 -0
- package/support/offer_bitcoin_network.js +45 -0
- package/support/offer_for_request.js +124 -0
- package/support/offer_records_for_payer.js +103 -0
- package/support/offer_suggested_amounts.js +75 -0
- package/support/offer_with_paths.js +53 -0
- package/support/open_support_note.js +81 -0
- package/support/payer_offer_args.js +39 -0
- package/support/payer_offer_for_request.js +171 -0
- package/support/payer_offer_signature_preimage.js +59 -0
- package/support/payer_offer_signer.js +95 -0
- package/support/payer_offer_types.js +21 -0
- package/support/read_menu_request.js +53 -0
- package/support/related_menu_levels.js +39 -0
- package/support/related_menus.js +171 -0
- package/support/reply_invoice_error.js +41 -0
- package/support/request_key.js +31 -0
- package/support/seal_support_note.js +63 -0
- package/support/service_support_offer.js +1032 -0
- package/support/shared_result.js +30 -0
- package/support/sign_invoice_for_request.js +125 -0
- package/support/sign_payer_offer.js +203 -0
- package/support/sign_support_menus.js +140 -0
- package/support/support_note_for_invoice.js +63 -0
- package/support/support_note_key.js +47 -0
- package/support/tlv_stream_records.js +26 -0
- package/support/unpaid_invoices.js +47 -0
- package/support/verify_menu_signature.js +44 -0
- package/support/verify_offer_reference.js +90 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
const asyncAuto = require('async/auto');
|
|
2
|
+
const {blindedPathFromHops} = require('bolt04');
|
|
3
|
+
const {getIdentity} = require('ln-service');
|
|
4
|
+
const {getPeers} = require('ln-service');
|
|
5
|
+
const {returnResult} = require('asyncjs-util');
|
|
6
|
+
|
|
7
|
+
const isHex = n => !(n.length % 2) && /^[0-9A-F]*$/i.test(n);
|
|
8
|
+
const isHexString = n => typeof n === 'string' && isHex(n);
|
|
9
|
+
const isLongPathId = n => n.length / 2 >= minPathIdBytes;
|
|
10
|
+
const isPublicKey = n => typeof n === 'string' && /^[0-9A-F]{66}$/i.test(n);
|
|
11
|
+
const minPathIdBytes = 16;
|
|
12
|
+
|
|
13
|
+
/** Create a blinded path to self that can be published to receive messages,
|
|
14
|
+
starting at a connected `introduction` peer, or at self
|
|
15
|
+
|
|
16
|
+
A given path `id` is at least 16 bytes from a secure random source, kept
|
|
17
|
+
private.
|
|
18
|
+
|
|
19
|
+
{
|
|
20
|
+
[id]: <Random Path Identifier Hex String>
|
|
21
|
+
[introduction]: <Introduction Connected Peer Public Key Hex String>
|
|
22
|
+
lnd: <Authenticated LND API Object>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@returns via cbk or Promise
|
|
26
|
+
{
|
|
27
|
+
id: <Path Identifier Hex String>
|
|
28
|
+
path: {
|
|
29
|
+
hops: [{
|
|
30
|
+
encrypted_data: <Encrypted Recipient Data Hex String>
|
|
31
|
+
relay_key: <Blinded Relaying Node Public Key Hex String>
|
|
32
|
+
}]
|
|
33
|
+
introduction_node: <Introduction Node Public Key Hex String>
|
|
34
|
+
key: <First Hop Path Key Public Key Hex String>
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
*/
|
|
38
|
+
module.exports = ({id, introduction, lnd}, cbk) => {
|
|
39
|
+
return new Promise((resolve, reject) => {
|
|
40
|
+
return asyncAuto({
|
|
41
|
+
// Check arguments
|
|
42
|
+
validate: cbk => {
|
|
43
|
+
if (!!id && !isHexString(id)) {
|
|
44
|
+
return cbk([400, 'ExpectedHexEncodedPathIdToCreateNetworkPath']);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// A path id is at least 16 random bytes, so it cannot be guessed
|
|
48
|
+
if (!!id && !isLongPathId(id)) {
|
|
49
|
+
return cbk([400, 'ExpectedLongerPathIdToCreateNetworkPath']);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!!introduction && !isPublicKey(introduction)) {
|
|
53
|
+
return cbk([400, 'ExpectedIntroductionPublicKeyToCreateNetworkPath']);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!lnd) {
|
|
57
|
+
return cbk([400, 'ExpectedLndToCreateNetworkPath']);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return cbk();
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
// Get the self identity key to use as the end of the path
|
|
64
|
+
getIdentity: ['validate', ({}, cbk) => getIdentity({lnd}, cbk)],
|
|
65
|
+
|
|
66
|
+
// Get the connected peers to confirm the introduction is connected
|
|
67
|
+
getPeers: ['validate', ({}, cbk) => {
|
|
68
|
+
// Exit early when there is no introduction peer
|
|
69
|
+
if (!introduction) {
|
|
70
|
+
return cbk();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return getPeers({lnd}, cbk);
|
|
74
|
+
}],
|
|
75
|
+
|
|
76
|
+
// Check the introduction is a connected peer, and get the path nodes
|
|
77
|
+
nodes: ['getIdentity', 'getPeers', ({getIdentity, getPeers}, cbk) => {
|
|
78
|
+
const identity = getIdentity.public_key;
|
|
79
|
+
|
|
80
|
+
// Exit early when the path starts and ends at self
|
|
81
|
+
if (!introduction) {
|
|
82
|
+
return cbk(null, {hops: [identity], introduction: identity});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Node keys are compared in lowercase, as LND gives them
|
|
86
|
+
const key = introduction.toLowerCase();
|
|
87
|
+
|
|
88
|
+
if (key === identity) {
|
|
89
|
+
return cbk([400, 'ExpectedIntroductionPeerOtherThanSelfForPath']);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const peerKeys = getPeers.peers.map(n => n.public_key);
|
|
93
|
+
|
|
94
|
+
// The introduction node needs to be able to forward to self
|
|
95
|
+
if (!peerKeys.includes(key)) {
|
|
96
|
+
return cbk([503, 'ExpectedConnectedIntroductionPeerToCreatePath']);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return cbk(null, {hops: [key, identity], introduction: key});
|
|
100
|
+
}],
|
|
101
|
+
|
|
102
|
+
// Create the blinded path
|
|
103
|
+
create: ['nodes', ({nodes}, cbk) => {
|
|
104
|
+
try {
|
|
105
|
+
const blinded = blindedPathFromHops({id, hops: nodes.hops});
|
|
106
|
+
|
|
107
|
+
return cbk(null, {
|
|
108
|
+
id: blinded.id,
|
|
109
|
+
path: {
|
|
110
|
+
hops: blinded.path,
|
|
111
|
+
introduction_node: nodes.introduction,
|
|
112
|
+
key: blinded.key,
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
} catch (err) {
|
|
116
|
+
return cbk([400, 'FailedToCreateNetworkPath', {err}]);
|
|
117
|
+
}
|
|
118
|
+
}],
|
|
119
|
+
},
|
|
120
|
+
returnResult({reject, resolve, of: 'create'}, cbk));
|
|
121
|
+
});
|
|
122
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const encodePeerResponse = require('./../p2p/encode_peer_response');
|
|
2
|
+
|
|
3
|
+
const defaultServerError = [500, 'InternalServiceError'];
|
|
4
|
+
|
|
5
|
+
/** Encode a response to a network request
|
|
6
|
+
|
|
7
|
+
When the response cannot be encoded, a generic server failure is encoded in
|
|
8
|
+
its place, and the encoding error is returned so that it can be reported
|
|
9
|
+
|
|
10
|
+
{
|
|
11
|
+
[failure]: [
|
|
12
|
+
<Failure Code Number>
|
|
13
|
+
<Failure Message String>
|
|
14
|
+
]
|
|
15
|
+
id: <Request Id Hex String>
|
|
16
|
+
[records]: [{
|
|
17
|
+
type: <Type Number String>
|
|
18
|
+
value: <Value Hex String>
|
|
19
|
+
}]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@returns
|
|
23
|
+
{
|
|
24
|
+
[err]: [<Error Code Number>, <Error Message String>]
|
|
25
|
+
message: <Encoded Response Hex String>
|
|
26
|
+
}
|
|
27
|
+
*/
|
|
28
|
+
module.exports = ({failure, id, records}) => {
|
|
29
|
+
try {
|
|
30
|
+
return {message: encodePeerResponse({failure, id, records}).message};
|
|
31
|
+
} catch (err) {
|
|
32
|
+
return {
|
|
33
|
+
err: [503, err.message],
|
|
34
|
+
message: encodePeerResponse({id, failure: defaultServerError}).message,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
};
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
const asyncAuto = require('async/auto');
|
|
2
|
+
const asyncRetry = require('async/retry');
|
|
3
|
+
const {getIdentity} = require('ln-service');
|
|
4
|
+
const {getPeers} = require('ln-service');
|
|
5
|
+
const {returnResult} = require('asyncjs-util');
|
|
6
|
+
|
|
7
|
+
const getRelayingRoute = require('./get_relaying_route');
|
|
8
|
+
|
|
9
|
+
const hasBit = (list, bits) => !!(list || []).find(n => bits.includes(n.bit));
|
|
10
|
+
const isRetry = err => !!err && err[1] === 'RetryWithoutIgnoredNodes';
|
|
11
|
+
const maxAttempts = 10;
|
|
12
|
+
const onlyChannelsBits = [66, 67];
|
|
13
|
+
|
|
14
|
+
/** Get a series of nodes to relay a message to a destination
|
|
15
|
+
|
|
16
|
+
A connected destination is sent to directly, unless it signals feature bit
|
|
17
|
+
66 or 67, `option_onion_messages_only_channels`, that it only takes
|
|
18
|
+
messages from peers with a channel. Otherwise the message goes back along a
|
|
19
|
+
payment route found from the destination to self, through nodes that relay
|
|
20
|
+
messages.
|
|
21
|
+
|
|
22
|
+
{
|
|
23
|
+
destination: <Destination Node Public Key Hex String>
|
|
24
|
+
lnd: <Authenticated LND API Object>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@returns via cbk or Promise
|
|
28
|
+
{
|
|
29
|
+
hops: [<Message Hop Public Key Hex String>]
|
|
30
|
+
}
|
|
31
|
+
*/
|
|
32
|
+
module.exports = ({destination, lnd}, cbk) => {
|
|
33
|
+
return new Promise((resolve, reject) => {
|
|
34
|
+
return asyncAuto({
|
|
35
|
+
// Check arguments
|
|
36
|
+
validate: cbk => {
|
|
37
|
+
// The destination key is a string so that it can be compared
|
|
38
|
+
if (!destination || typeof destination !== 'string') {
|
|
39
|
+
return cbk([400, 'ExpectedDestinationToGetMessageHops']);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!lnd) {
|
|
43
|
+
return cbk([400, 'ExpectedLndToGetMessageHops']);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return cbk();
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
// Node keys are compared in lowercase, as LND gives them
|
|
50
|
+
key: ['validate', ({}, cbk) => cbk(null, destination.toLowerCase())],
|
|
51
|
+
|
|
52
|
+
// Get the connected peers
|
|
53
|
+
getPeers: ['validate', ({}, cbk) => getPeers({lnd}, cbk)],
|
|
54
|
+
|
|
55
|
+
// A connected destination is sent to directly unless it needs a channel
|
|
56
|
+
isDirect: ['getPeers', 'key', ({getPeers, key}, cbk) => {
|
|
57
|
+
const peer = getPeers.peers.find(n => n.public_key === key);
|
|
58
|
+
|
|
59
|
+
return cbk(null, !!peer && !hasBit(peer.features, onlyChannelsBits));
|
|
60
|
+
}],
|
|
61
|
+
|
|
62
|
+
// Get the identity key of self, which routes are found back to
|
|
63
|
+
getIdentity: ['isDirect', ({isDirect}, cbk) => {
|
|
64
|
+
// Exit early when the destination is sent to directly
|
|
65
|
+
if (isDirect) {
|
|
66
|
+
return cbk();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return getIdentity({lnd}, cbk);
|
|
70
|
+
}],
|
|
71
|
+
|
|
72
|
+
// Find a series of nodes to relay the message
|
|
73
|
+
hops: [
|
|
74
|
+
'getIdentity',
|
|
75
|
+
'getPeers',
|
|
76
|
+
'isDirect',
|
|
77
|
+
'key',
|
|
78
|
+
({getIdentity, getPeers, isDirect, key}, cbk) =>
|
|
79
|
+
{
|
|
80
|
+
// Exit early when the destination is sent to directly
|
|
81
|
+
if (isDirect) {
|
|
82
|
+
return cbk(null, {hops: [key]});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const source = getIdentity.public_key;
|
|
86
|
+
|
|
87
|
+
if (key === source) {
|
|
88
|
+
return cbk([400, 'UnexpectedMessageDestinationIsSelf']);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const ignore = [];
|
|
92
|
+
|
|
93
|
+
// Find a route of nodes that relay messages, trying again without the
|
|
94
|
+
// nodes that do not
|
|
95
|
+
return asyncRetry({errorFilter: isRetry, times: maxAttempts}, cbk => {
|
|
96
|
+
return getRelayingRoute({
|
|
97
|
+
ignore,
|
|
98
|
+
lnd,
|
|
99
|
+
source,
|
|
100
|
+
destination: key,
|
|
101
|
+
peers: getPeers.peers,
|
|
102
|
+
},
|
|
103
|
+
(err, res) => {
|
|
104
|
+
if (!!err) {
|
|
105
|
+
return cbk(err);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Exit early when the route has nodes to use
|
|
109
|
+
if (!!res.hops) {
|
|
110
|
+
return cbk(null, {hops: res.hops});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
res.ignore.forEach(n => ignore.push(n));
|
|
114
|
+
|
|
115
|
+
return cbk([503, 'RetryWithoutIgnoredNodes']);
|
|
116
|
+
});
|
|
117
|
+
},
|
|
118
|
+
(err, res) => {
|
|
119
|
+
// Exit early when every attempt found nodes that do not relay
|
|
120
|
+
if (isRetry(err)) {
|
|
121
|
+
return cbk([503, 'FailedToFindMessageHopsToDestination']);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return cbk(err, res);
|
|
125
|
+
});
|
|
126
|
+
}],
|
|
127
|
+
},
|
|
128
|
+
returnResult({reject, resolve, of: 'hops'}, cbk));
|
|
129
|
+
});
|
|
130
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
const asyncAuto = require('async/auto');
|
|
2
|
+
const {decryptBlindedPath} = require('bolt04');
|
|
3
|
+
const {diffieHellmanComputeSecret} = require('ln-service');
|
|
4
|
+
const {returnResult} = require('asyncjs-util');
|
|
5
|
+
|
|
6
|
+
/** Get the path id of a received message from its encrypted recipient data
|
|
7
|
+
|
|
8
|
+
{
|
|
9
|
+
encrypted: <Encrypted Recipient Data Hex String>
|
|
10
|
+
key: <Path Key Hex String>
|
|
11
|
+
lnd: <Authenticated LND API Object>
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
@returns via cbk or Promise
|
|
15
|
+
{
|
|
16
|
+
[id]: <Path Identifier Hex String>
|
|
17
|
+
}
|
|
18
|
+
*/
|
|
19
|
+
module.exports = ({encrypted, key, lnd}, cbk) => {
|
|
20
|
+
return new Promise((resolve, reject) => {
|
|
21
|
+
return asyncAuto({
|
|
22
|
+
// Check arguments
|
|
23
|
+
validate: cbk => {
|
|
24
|
+
if (!encrypted) {
|
|
25
|
+
return cbk([400, 'ExpectedEncryptedRecipientDataToGetPathId']);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!key) {
|
|
29
|
+
return cbk([400, 'ExpectedPathKeyToGetPathId']);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!lnd) {
|
|
33
|
+
return cbk([400, 'ExpectedLndToGetPathId']);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return cbk();
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
// Compute the shared secret of the path key and the node identity key
|
|
40
|
+
getSecret: ['validate', ({}, cbk) => {
|
|
41
|
+
return diffieHellmanComputeSecret({lnd, partner_public_key: key}, cbk);
|
|
42
|
+
}],
|
|
43
|
+
|
|
44
|
+
// Decrypt the recipient data to get the path id
|
|
45
|
+
decrypt: ['getSecret', ({getSecret}, cbk) => {
|
|
46
|
+
try {
|
|
47
|
+
const {id} = decryptBlindedPath({
|
|
48
|
+
encrypted,
|
|
49
|
+
key,
|
|
50
|
+
secret: getSecret.secret,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
return cbk(null, {id});
|
|
54
|
+
} catch (err) {
|
|
55
|
+
return cbk([503, 'FailedToDecryptRecipientDataForPathId', {err}]);
|
|
56
|
+
}
|
|
57
|
+
}],
|
|
58
|
+
},
|
|
59
|
+
returnResult({reject, resolve, of: 'decrypt'}, cbk));
|
|
60
|
+
});
|
|
61
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
const asyncAuto = require('async/auto');
|
|
2
|
+
const {getChannel} = require('ln-service');
|
|
3
|
+
const {returnResult} = require('asyncjs-util');
|
|
4
|
+
|
|
5
|
+
const getMessageHops = require('./get_message_hops');
|
|
6
|
+
const pathIntroduction = require('./path_introduction');
|
|
7
|
+
|
|
8
|
+
const edgeChannel = edge => edge.split('x').slice(0, -1).join('x');
|
|
9
|
+
|
|
10
|
+
/** Get the outbound hops to reach the introduction node of a blinded path
|
|
11
|
+
|
|
12
|
+
When the introduction node is not a connected peer, the message is relayed
|
|
13
|
+
along a payment route through nodes that relay onion messages
|
|
14
|
+
|
|
15
|
+
{
|
|
16
|
+
lnd: <Authenticated LND API Object>
|
|
17
|
+
path: {
|
|
18
|
+
[introduction_edge]: <Introduction Node Edge Format Channel Id String>
|
|
19
|
+
[introduction_node]: <Introduction Node Public Key Hex String>
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@returns via cbk or Promise
|
|
24
|
+
{
|
|
25
|
+
outbound: [<Relaying Node Public Key Hex String>]
|
|
26
|
+
}
|
|
27
|
+
*/
|
|
28
|
+
module.exports = ({lnd, path}, cbk) => {
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
return asyncAuto({
|
|
31
|
+
// Check arguments
|
|
32
|
+
validate: cbk => {
|
|
33
|
+
if (!lnd) {
|
|
34
|
+
return cbk([400, 'ExpectedLndToGetPathOutbound']);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!path) {
|
|
38
|
+
return cbk([400, 'ExpectedBlindedPathToGetPathOutbound']);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return cbk();
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
// Get the channel of the introduction edge
|
|
45
|
+
getChannel: ['validate', ({}, cbk) => {
|
|
46
|
+
// Exit early when the introduction is a node and not an edge
|
|
47
|
+
if (!path.introduction_edge) {
|
|
48
|
+
return cbk();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const id = edgeChannel(path.introduction_edge);
|
|
52
|
+
|
|
53
|
+
return getChannel({id, lnd}, cbk);
|
|
54
|
+
}],
|
|
55
|
+
|
|
56
|
+
// Determine the introduction node of the path
|
|
57
|
+
introduction: ['getChannel', ({getChannel}, cbk) => {
|
|
58
|
+
try {
|
|
59
|
+
const {introduction} = pathIntroduction({
|
|
60
|
+
path,
|
|
61
|
+
channel: getChannel,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
return cbk(null, introduction);
|
|
65
|
+
} catch (err) {
|
|
66
|
+
return cbk([400, err.message]);
|
|
67
|
+
}
|
|
68
|
+
}],
|
|
69
|
+
|
|
70
|
+
// Get the hops to relay the message to the introduction node
|
|
71
|
+
getHops: ['introduction', ({introduction}, cbk) => {
|
|
72
|
+
return getMessageHops({lnd, destination: introduction}, cbk);
|
|
73
|
+
}],
|
|
74
|
+
|
|
75
|
+
// The outbound hops end at the introduction node
|
|
76
|
+
outbound: ['getHops', ({getHops}, cbk) => {
|
|
77
|
+
return cbk(null, {outbound: getHops.hops});
|
|
78
|
+
}],
|
|
79
|
+
},
|
|
80
|
+
returnResult({reject, resolve, of: 'outbound'}, cbk));
|
|
81
|
+
});
|
|
82
|
+
};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
const asyncAuto = require('async/auto');
|
|
2
|
+
const asyncMap = require('async/map');
|
|
3
|
+
const {getNode} = require('ln-service');
|
|
4
|
+
const {getRouteToDestination} = require('ln-service');
|
|
5
|
+
const {returnResult} = require('asyncjs-util');
|
|
6
|
+
|
|
7
|
+
const relaysAsHops = require('./relays_as_hops');
|
|
8
|
+
const routeRelays = require('./route_relays');
|
|
9
|
+
|
|
10
|
+
const tokens = 1;
|
|
11
|
+
|
|
12
|
+
/** Get a route of nodes that relay a message to a destination, from a
|
|
13
|
+
payment route found from the destination back to self
|
|
14
|
+
|
|
15
|
+
When the route cannot be used, the nodes to avoid in the next route are
|
|
16
|
+
given instead.
|
|
17
|
+
|
|
18
|
+
{
|
|
19
|
+
destination: <Destination Node Public Key Hex String>
|
|
20
|
+
ignore: [{
|
|
21
|
+
[from_public_key]: <Avoid Route From Node Public Key Hex String>
|
|
22
|
+
[to_public_key]: <Avoid Route To Node Public Key Hex String>
|
|
23
|
+
}]
|
|
24
|
+
lnd: <Authenticated LND API Object>
|
|
25
|
+
peers: [{
|
|
26
|
+
public_key: <Peer Public Key Hex String>
|
|
27
|
+
}]
|
|
28
|
+
source: <Self Public Key Hex String>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@returns via cbk or Promise
|
|
32
|
+
{
|
|
33
|
+
[hops]: [<Message Hop Public Key Hex String>]
|
|
34
|
+
[ignore]: [{
|
|
35
|
+
[from_public_key]: <Avoid Route From Node Public Key Hex String>
|
|
36
|
+
[to_public_key]: <Avoid Route To Node Public Key Hex String>
|
|
37
|
+
}]
|
|
38
|
+
}
|
|
39
|
+
*/
|
|
40
|
+
module.exports = ({destination, ignore, lnd, peers, source}, cbk) => {
|
|
41
|
+
return new Promise((resolve, reject) => {
|
|
42
|
+
return asyncAuto({
|
|
43
|
+
// Find a route from the destination back to self
|
|
44
|
+
getRoute: cbk => {
|
|
45
|
+
return getRouteToDestination({
|
|
46
|
+
ignore,
|
|
47
|
+
lnd,
|
|
48
|
+
tokens,
|
|
49
|
+
destination: source,
|
|
50
|
+
is_ignoring_past_failures: true,
|
|
51
|
+
start: destination,
|
|
52
|
+
},
|
|
53
|
+
cbk);
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
// Get the nodes that the message goes through on the route
|
|
57
|
+
relays: ['getRoute', ({getRoute}, cbk) => {
|
|
58
|
+
// Exit early when there is no route
|
|
59
|
+
if (!getRoute.route) {
|
|
60
|
+
return cbk([503, 'FailedToFindMessageHopsToDestination']);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return cbk(null, routeRelays({
|
|
64
|
+
destination,
|
|
65
|
+
peers,
|
|
66
|
+
source,
|
|
67
|
+
route: getRoute.route,
|
|
68
|
+
}));
|
|
69
|
+
}],
|
|
70
|
+
|
|
71
|
+
// Get the features of each node, as no features when not known
|
|
72
|
+
getNodes: ['relays', ({relays}, cbk) => {
|
|
73
|
+
return asyncMap(relays.relays || [], (key, cbk) => {
|
|
74
|
+
return getNode({
|
|
75
|
+
lnd,
|
|
76
|
+
is_omitting_channels: true,
|
|
77
|
+
public_key: key,
|
|
78
|
+
},
|
|
79
|
+
(err, res) => cbk(null, !!err ? {features: []} : res));
|
|
80
|
+
},
|
|
81
|
+
cbk);
|
|
82
|
+
}],
|
|
83
|
+
|
|
84
|
+
// Check that every node on the route relays messages
|
|
85
|
+
route: ['getNodes', 'relays', ({getNodes, relays}, cbk) => {
|
|
86
|
+
// Exit early when the first hop is not a peer
|
|
87
|
+
if (!!relays.ignore) {
|
|
88
|
+
return cbk(null, {ignore: relays.ignore});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return cbk(null, relaysAsHops({
|
|
92
|
+
destination,
|
|
93
|
+
nodes: getNodes,
|
|
94
|
+
relays: relays.relays,
|
|
95
|
+
}));
|
|
96
|
+
}],
|
|
97
|
+
},
|
|
98
|
+
returnResult({reject, resolve, of: 'route'}, cbk));
|
|
99
|
+
});
|
|
100
|
+
};
|
package/network/index.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const createNetworkPath = require('./create_network_path');
|
|
2
|
+
const makeNetworkRequest = require('./make_network_request');
|
|
3
|
+
const requestNetworkReply = require('./request_network_reply');
|
|
4
|
+
const resolvePathIntroductions = require('./resolve_path_introductions');
|
|
5
|
+
const serviceNetworkMessages = require('./service_network_messages');
|
|
6
|
+
const serviceNetworkRequests = require('./service_network_requests');
|
|
7
|
+
|
|
8
|
+
module.exports = {
|
|
9
|
+
createNetworkPath,
|
|
10
|
+
makeNetworkRequest,
|
|
11
|
+
requestNetworkReply,
|
|
12
|
+
resolvePathIntroductions,
|
|
13
|
+
serviceNetworkMessages,
|
|
14
|
+
serviceNetworkRequests,
|
|
15
|
+
};
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
const {randomBytes} = require('crypto');
|
|
2
|
+
|
|
3
|
+
const asyncAuto = require('async/auto');
|
|
4
|
+
const {returnResult} = require('asyncjs-util');
|
|
5
|
+
|
|
6
|
+
const encodePeerRequest = require('./../p2p/encode_peer_request');
|
|
7
|
+
const parseRequestMessage = require('./../p2p/parse_request_message');
|
|
8
|
+
const requestNetworkReply = require('./request_network_reply');
|
|
9
|
+
|
|
10
|
+
const {isArray} = Array;
|
|
11
|
+
const isPublicKey = n => typeof n === 'string' && /^[0-9A-F]{66}$/i.test(n);
|
|
12
|
+
const {isSafeInteger} = Number;
|
|
13
|
+
const isTimeout = n => isSafeInteger(n) && n > 0;
|
|
14
|
+
const bufferAsHex = buffer => Buffer.from(buffer).toString('hex');
|
|
15
|
+
const makeId = () => bufferAsHex(randomBytes(32));
|
|
16
|
+
const peerRequestType = 32768;
|
|
17
|
+
const typeNetworkRequest = '32769';
|
|
18
|
+
|
|
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 to self
|
|
21
|
+
|
|
22
|
+
The request fails when no response arrives within the `timeout`.
|
|
23
|
+
|
|
24
|
+
{
|
|
25
|
+
lnd: <Authenticated LND API Object>
|
|
26
|
+
path: {
|
|
27
|
+
hops: [{
|
|
28
|
+
encrypted_data: <Encrypted Recipient Data Hex String>
|
|
29
|
+
relay_key: <Blinded Relaying Node Public Key Hex String>
|
|
30
|
+
}]
|
|
31
|
+
[introduction_edge]: <Introduction Node Edge Format Channel Id String>
|
|
32
|
+
[introduction_node]: <Introduction Node Public Key Hex String>
|
|
33
|
+
key: <First Hop Path Key Public Key Hex String>
|
|
34
|
+
}
|
|
35
|
+
[records]: [{
|
|
36
|
+
type: <Type Number String>
|
|
37
|
+
value: <Value Hex Encoded String>
|
|
38
|
+
}]
|
|
39
|
+
[timeout]: <Response Timeout Milliseconds Number> // Default: 30000
|
|
40
|
+
type: <Request Type Number String>
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@throws error via cbk or Promise
|
|
44
|
+
[0, NetworkRequestTimeout]
|
|
45
|
+
|
|
46
|
+
@returns via cbk or Promise
|
|
47
|
+
{
|
|
48
|
+
[records]: [{
|
|
49
|
+
type: <Type Number String>
|
|
50
|
+
value: <Value Hex Encoded String>
|
|
51
|
+
}]
|
|
52
|
+
}
|
|
53
|
+
*/
|
|
54
|
+
module.exports = ({lnd, path, records, timeout, type}, cbk) => {
|
|
55
|
+
return new Promise((resolve, reject) => {
|
|
56
|
+
return asyncAuto({
|
|
57
|
+
// Check arguments
|
|
58
|
+
validate: cbk => {
|
|
59
|
+
if (!lnd) {
|
|
60
|
+
return cbk([400, 'ExpectedLndToMakeNetworkRequest']);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!path) {
|
|
64
|
+
return cbk([400, 'ExpectedBlindedPathToMakeNetworkRequest']);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!isArray(path.hops) || !path.hops.length) {
|
|
68
|
+
return cbk([400, 'ExpectedBlindedPathHopsToMakeNetworkRequest']);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!path.introduction_edge && !path.introduction_node) {
|
|
72
|
+
return cbk([400, 'ExpectedPathIntroductionToMakeNetworkRequest']);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// A node introduction is looked up as a public key
|
|
76
|
+
if (!!path.introduction_node && !isPublicKey(path.introduction_node)) {
|
|
77
|
+
return cbk([400, 'ExpectedIntroductionKeyToMakeNetworkRequest']);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!!path.introduction_edge) {
|
|
81
|
+
if (typeof path.introduction_edge !== 'string') {
|
|
82
|
+
return cbk([400, 'ExpectedIntroductionEdgeToMakeNetworkRequest']);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (!path.key) {
|
|
87
|
+
return cbk([400, 'ExpectedBlindedPathKeyToMakeNetworkRequest']);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (timeout !== undefined && !isTimeout(timeout)) {
|
|
91
|
+
return cbk([400, 'ExpectedPositiveTimeoutToMakeNetworkRequest']);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!type) {
|
|
95
|
+
return cbk([400, 'ExpectedTypeToMakeNetworkRequest']);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return cbk();
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
// Generate an id for the request
|
|
102
|
+
id: ['validate', ({}, cbk) => cbk(null, makeId())],
|
|
103
|
+
|
|
104
|
+
// Encode the request message
|
|
105
|
+
message: ['id', ({id}, cbk) => {
|
|
106
|
+
try {
|
|
107
|
+
const {message} = encodePeerRequest({id, records, type});
|
|
108
|
+
|
|
109
|
+
return cbk(null, {type: typeNetworkRequest, value: message});
|
|
110
|
+
} catch (err) {
|
|
111
|
+
return cbk([400, err.message]);
|
|
112
|
+
}
|
|
113
|
+
}],
|
|
114
|
+
|
|
115
|
+
// Send the request and wait for the reply on its reply path
|
|
116
|
+
getReply: ['message', ({message}, cbk) => {
|
|
117
|
+
return requestNetworkReply({
|
|
118
|
+
lnd,
|
|
119
|
+
message,
|
|
120
|
+
path,
|
|
121
|
+
timeout,
|
|
122
|
+
types: [typeNetworkRequest],
|
|
123
|
+
},
|
|
124
|
+
cbk);
|
|
125
|
+
}],
|
|
126
|
+
|
|
127
|
+
// Read the response to the request
|
|
128
|
+
request: ['getReply', 'id', ({getReply, id}, cbk) => {
|
|
129
|
+
try {
|
|
130
|
+
const {response} = parseRequestMessage({
|
|
131
|
+
message: getReply.reply.value,
|
|
132
|
+
type: peerRequestType,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
if (!response || response.id !== id) {
|
|
136
|
+
return cbk([503, 'ExpectedResponseToNetworkRequest']);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Exit early when the server responds with a failure code
|
|
140
|
+
if (!!response.failure) {
|
|
141
|
+
return cbk(response.failure);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return cbk(null, {records: response.records});
|
|
145
|
+
} catch (err) {
|
|
146
|
+
return cbk([503, 'ExpectedResponseToNetworkRequest']);
|
|
147
|
+
}
|
|
148
|
+
}],
|
|
149
|
+
},
|
|
150
|
+
returnResult({reject, resolve, of: 'request'}, cbk));
|
|
151
|
+
});
|
|
152
|
+
};
|