paid-services 10.2.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 CHANGED
@@ -1,5 +1,10 @@
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
+
3
8
  ## Version 10.2.0
4
9
 
5
10
  - `serviceSupportOffer`: Add `start` event with the support offer
@@ -12,5 +12,7 @@
12
12
  "codeInvalidArgument": 3,
13
13
  "codePermissionDenied": 7,
14
14
  "codeUnauthenticated": 16,
15
- "codeUnimplemented": 12
15
+ "codeUnimplemented": 12,
16
+ "knownHopRecordTypes": ["1", "2", "4", "6", "8", "10", "12", "14"],
17
+ "typePathId": "6"
16
18
  }
@@ -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,28 +1,48 @@
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
  {
16
26
  [is_expecting_reply]: <Reply Comes Back Along the Same Nodes Bool>
17
27
  lnd: <Authenticated LND API Object>
18
28
  path: {
29
+ hops: [{
30
+ encrypted_data: <Encrypted Recipient Data Hex String>
31
+ relay_key: <Blinded Relaying Node Public Key Hex String>
32
+ }]
19
33
  [introduction_edge]: <Introduction Node Edge Format Channel Id String>
20
34
  [introduction_node]: <Introduction Node Public Key Hex String>
35
+ key: <First Hop Path Key Public Key Hex String>
21
36
  }
22
37
  }
23
38
 
24
39
  @returns via cbk or Promise
25
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>
26
46
  outbound: [<Relaying Node Public Key Hex String>]
27
47
  }
28
48
  */
@@ -39,6 +59,21 @@ module.exports = (args, cbk) => {
39
59
  return cbk([400, 'ExpectedBlindedPathToGetPathOutbound']);
40
60
  }
41
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
+
42
77
  return cbk();
43
78
  },
44
79
 
@@ -54,6 +89,11 @@ module.exports = (args, cbk) => {
54
89
  return getChannel({id, lnd: args.lnd}, cbk);
55
90
  }],
56
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);
95
+ }],
96
+
57
97
  // Determine the introduction node of the path
58
98
  introduction: ['getChannel', ({getChannel}, cbk) => {
59
99
  try {
@@ -62,25 +102,69 @@ module.exports = (args, cbk) => {
62
102
  path: args.path,
63
103
  });
64
104
 
65
- return cbk(null, introduction);
105
+ // Node keys are compared in lowercase, as LND gives them
106
+ return cbk(null, introduction.toLowerCase());
66
107
  } catch (err) {
67
108
  return cbk([400, err.message]);
68
109
  }
69
110
  }],
70
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
+
71
151
  // Get the hops to relay the message to the introduction node
72
- getHops: ['introduction', ({introduction}, cbk) => {
152
+ getHops: ['stepped', ({stepped}, cbk) => {
73
153
  return getMessageHops({
74
- destination: introduction,
154
+ destination: stepped.introduction_node,
75
155
  is_expecting_reply: args.is_expecting_reply,
76
156
  lnd: args.lnd,
77
157
  },
78
158
  cbk);
79
159
  }],
80
160
 
81
- // The outbound hops end at the introduction node
82
- outbound: ['getHops', ({getHops}, cbk) => {
83
- return cbk(null, {outbound: getHops.hops});
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
+ });
84
168
  }],
85
169
  },
86
170
  returnResult({reject, resolve, of: 'outbound'}, cbk));
@@ -27,6 +27,9 @@ const isTimeout = n => isSafeInteger(n) && n > 0;
27
27
  than `max_reply_hops` keeps the hops nearest self, and the recipient routes
28
28
  to its start.
29
29
 
30
+ A path that starts at self is sent from the next node of the path, and the
31
+ reply path goes back from there.
32
+
30
33
  {
31
34
  lnd: <Authenticated LND API Object>
32
35
  [max_reply_hops]: <Maximum Reply Path Hops Count Number>
@@ -135,9 +138,9 @@ module.exports = (args, cbk) => {
135
138
  {
136
139
  const {outbound} = getOutbound;
137
140
 
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
+ // The outbound hops end at the introduction node of the hops that are
142
+ // sent, which is the recipient itself when there is a single hop
143
+ const isRecipient = getOutbound.hops.length === 1;
141
144
 
142
145
  const relays = !isRecipient ? outbound : outbound.slice(0, -1);
143
146
 
@@ -258,8 +261,8 @@ module.exports = (args, cbk) => {
258
261
 
259
262
  // Send the message with a reply path back the way it goes
260
263
  const sending = sendMessage({
261
- inbound: args.path.hops,
262
- key: args.path.key,
264
+ inbound: getOutbound.hops,
265
+ key: getOutbound.key,
263
266
  lnd: args.lnd,
264
267
  message: args.message,
265
268
  outbound: getOutbound.outbound,
@@ -14,8 +14,8 @@ const largeOnionBytes = 32834;
14
14
 
15
15
  With `is_regular`, the response is only sent when it fits in an onion
16
16
  message of the regular size. Otherwise it has to fit in a large one. A
17
- response that does not fit is a 413 error. Give the `outbound` hops to the
18
- reply path when they are known.
17
+ response that does not fit is a 413 error. Give the `route` to the reply
18
+ path when it is known, as found by `getPathOutbound`.
19
19
 
20
20
  {
21
21
  [is_regular]: <Only Send In An Onion Message Of The Regular Size Bool>
@@ -33,7 +33,14 @@ const largeOnionBytes = 32834;
33
33
  [introduction_node]: <Introduction Node Public Key Hex String>
34
34
  key: <Path Key Hex String>
35
35
  }
36
- [outbound]: [<Relaying Node Public Key Hex String>]
36
+ [route]: {
37
+ hops: [{
38
+ encrypted_data: <Encrypted Data Hex String>
39
+ relay_key: <Blinded Relay Key Hex String>
40
+ }]
41
+ key: <Path Key Hex String>
42
+ outbound: [<Relaying Node Public Key Hex String>]
43
+ }
37
44
  }
38
45
 
39
46
  @returns via cbk or Promise
@@ -58,23 +65,32 @@ module.exports = (args, cbk) => {
58
65
  return cbk();
59
66
  },
60
67
 
61
- // Determine the outbound hops to reach the reply path
62
- getOutbound: ['validate', ({}, cbk) => {
63
- // Exit early when the outbound hops are already known
64
- if (!!args.outbound) {
65
- return cbk(null, {outbound: args.outbound});
68
+ // Determine the route to the reply path: the hops and key to send over
69
+ getRoute: ['validate', ({}, cbk) => {
70
+ // Exit early when the route is already known
71
+ if (!!args.route) {
72
+ return cbk(null, args.route);
66
73
  }
67
74
 
68
- return getPathOutbound({lnd: args.lnd, path: args.reply}, cbk);
75
+ return getPathOutbound({
76
+ lnd: args.lnd,
77
+ path: {
78
+ hops: args.reply.inbound,
79
+ introduction_edge: args.reply.introduction_edge,
80
+ introduction_node: args.reply.introduction_node,
81
+ key: args.reply.key,
82
+ },
83
+ },
84
+ cbk);
69
85
  }],
70
86
 
71
87
  // Check that the response fits in an onion message that can be sent
72
- checkSize: ['getOutbound', ({getOutbound}, cbk) => {
88
+ checkSize: ['getRoute', ({getRoute}, cbk) => {
73
89
  try {
74
90
  const {onion} = onionForPath({
75
- inbound: args.reply.inbound,
76
- key: args.reply.key,
77
- outbound: getOutbound.outbound,
91
+ inbound: getRoute.hops,
92
+ key: getRoute.key,
93
+ outbound: getRoute.outbound,
78
94
  records: [args.message],
79
95
  });
80
96
 
@@ -99,13 +115,13 @@ module.exports = (args, cbk) => {
99
115
  }],
100
116
 
101
117
  // Send the response message through the reply path
102
- send: ['checkSize', 'getOutbound', ({getOutbound}, cbk) => {
118
+ send: ['checkSize', 'getRoute', ({getRoute}, cbk) => {
103
119
  return sendMessage({
104
- inbound: args.reply.inbound,
105
- key: args.reply.key,
120
+ inbound: getRoute.hops,
121
+ key: getRoute.key,
106
122
  lnd: args.lnd,
107
123
  message: args.message,
108
- outbound: getOutbound.outbound,
124
+ outbound: getRoute.outbound,
109
125
  },
110
126
  cbk);
111
127
  }],
@@ -176,23 +176,32 @@ module.exports = ({delay, ids, lnd}) => {
176
176
 
177
177
  const {encrypted, key, reply} = received;
178
178
 
179
- let outbound;
179
+ let route;
180
180
 
181
- // The route to the reply path is found once for a message
182
- const getOutbound = () => {
181
+ // The route to the reply path, the hops and key to send over and the
182
+ // outbound hops to reach them, is found once for a message
183
+ const getRoute = () => {
183
184
  // Exit early when the route was already found or is being found
184
- if (!!outbound) {
185
- return outbound;
185
+ if (!!route) {
186
+ return route;
186
187
  }
187
188
 
188
- const lookup = getPathOutbound({lnd, path: reply});
189
+ const lookup = getPathOutbound({
190
+ lnd,
191
+ path: {
192
+ hops: reply.inbound,
193
+ introduction_edge: reply.introduction_edge,
194
+ introduction_node: reply.introduction_node,
195
+ key: reply.key,
196
+ },
197
+ });
189
198
 
190
- outbound = lookup;
199
+ route = lookup;
191
200
 
192
201
  // A failed lookup is not kept, so that a later reply can try again
193
202
  lookup.catch(() => {
194
- if (outbound === lookup) {
195
- outbound = undefined;
203
+ if (route === lookup) {
204
+ route = undefined;
196
205
  }
197
206
  });
198
207
 
@@ -224,7 +233,7 @@ module.exports = ({delay, ids, lnd}) => {
224
233
 
225
234
  // The caller is told outside of the promise chain, so that a throw in
226
235
  // the caller is not taken as a failure to send
227
- return getOutbound()
236
+ return getRoute()
228
237
  .then(res => {
229
238
  // Exit early when the service stopped while the route was found
230
239
  if (isStopped) {
@@ -236,7 +245,7 @@ module.exports = ({delay, ids, lnd}) => {
236
245
  lnd,
237
246
  reply,
238
247
  message: response,
239
- outbound: res.outbound,
248
+ route: res,
240
249
  });
241
250
  })
242
251
  .then(() => setImmediate(() => done()), err => {
@@ -253,11 +262,11 @@ module.exports = ({delay, ids, lnd}) => {
253
262
 
254
263
  // The caller is told outside of the promise chain, so that it is only
255
264
  // called once, even when it throws
256
- return getOutbound()
265
+ return getRoute()
257
266
  .then(res => regularReplyBytes({
258
267
  type,
259
- inbound: reply.inbound,
260
- key: reply.key,
268
+ inbound: res.hops,
269
+ key: res.key,
261
270
  outbound: res.outbound,
262
271
  }))
263
272
  .then(res => setImmediate(() => cbk(null, res)), err => {
@@ -29,11 +29,11 @@ const typeSchnorr = 'schnorr';
29
29
  The request is sent over each path of the offer in turn, waiting `timeout`
30
30
  for each, until a path gives back an invoice, or the recipient replies
31
31
  with an invoice error, which is returned. An offer without paths has the
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.
32
+ request sent to its issuer id. A path that starts at the payer is sent
33
+ from its next node. The reply path of a request goes back along its route,
34
+ so that the recipient can reach it. The route to the start of a path is
35
+ found over the public graph and the channels of the payer, so a payer that
36
+ is only connected through a peer that is itself private is not supported.
37
37
 
38
38
  The request is signed by the `payer_id`, a new key that LND derives in key
39
39
  family 808 at `payer_key_index`, only once the request is known to be one
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.2.0"
76
+ "version": "10.3.0"
77
77
  }