paid-services 10.0.0 → 10.1.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,13 @@
1
1
  # Versions
2
2
 
3
+ ## Version 10.1.0
4
+
5
+ - `createSupportOffer`: Add `introduction` to start the created offer path at
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
10
+
3
11
  ## Version 10.0.0
4
12
 
5
13
  - `getOfferInvoice`: Add `features` to show the features of the invoice
@@ -4,6 +4,7 @@ const {createSignedInvoiceRequest} = require('invoices');
4
4
  const {getNetwork} = require('ln-sync');
5
5
  const {getPeers} = require('ln-service');
6
6
  const {getPublicKey} = require('ln-service');
7
+ const {pointFromScalar} = require('tiny-secp256k1');
7
8
  const {returnResult} = require('asyncjs-util');
8
9
  const {signBytes} = require('ln-service');
9
10
 
@@ -14,8 +15,10 @@ const requestOfferInvoice = require('./request_offer_invoice');
14
15
  const {resolvePathIntroductions} = require('./../network');
15
16
 
16
17
  const {isSafeInteger} = Number;
18
+ const bufferAsHex = buffer => Buffer.from(buffer).toString('hex');
17
19
  const defaultTimeoutMs = 30000;
18
20
  const isTimeout = n => isSafeInteger(n) && n > 0;
21
+ const placeholderPayerId = bufferAsHex(pointFromScalar(Buffer.alloc(32, 1)));
19
22
  const typeSchnorr = 'schnorr';
20
23
 
21
24
  /** Get an invoice for a BOLT 12 offer
@@ -30,9 +33,10 @@ const typeSchnorr = 'schnorr';
30
33
  request sent to its issuer id.
31
34
 
32
35
  The request is signed by the `payer_id`, a new key that LND derives in key
33
- family 808 at `payer_key_index`. Keep the index to be able to prove, with
34
- the key derived again from the seed, that the request and the payment of
35
- its invoice were made by the payer.
36
+ family 808 at `payer_key_index`, only once the request is known to be one
37
+ that can be made, so that a refused request uses no index. Keep the index
38
+ to be able to prove, with the key derived again from the seed, that the
39
+ request and the payment of its invoice were made by the payer.
36
40
 
37
41
  {
38
42
  [bip353_name]: <BIP 353 Human Readable Name Offer Was Found With String>
@@ -98,12 +102,48 @@ module.exports = (args, cbk) => {
98
102
  // Get the connected peers to start the reply path at one of them
99
103
  getPeers: ['validate', ({}, cbk) => getPeers({lnd: args.lnd}, cbk)],
100
104
 
105
+ // Make the request with a placeholder payer id, to check that it can be
106
+ // made before a payer key is derived for it
107
+ checkRequest: ['getNetwork', ({getNetwork}, cbk) => {
108
+ try {
109
+ return cbk(null, invoiceRequestForOffer({
110
+ bip353_name: args.bip353_name,
111
+ mtokens: args.mtokens,
112
+ network: getNetwork.bitcoinjs,
113
+ offer: args.offer,
114
+ payer_id: placeholderPayerId,
115
+ payer_note: args.payer_note,
116
+ }));
117
+ } catch (err) {
118
+ return cbk([400, err.message]);
119
+ }
120
+ }],
121
+
122
+ // Find the introduction node of each offer path, to not reply from it
123
+ getPaths: ['checkRequest', ({checkRequest}, cbk) => {
124
+ return resolvePathIntroductions({
125
+ lnd: args.lnd,
126
+ paths: checkRequest.paths,
127
+ },
128
+ cbk);
129
+ }],
130
+
131
+ // The offer paths to send the request over
132
+ offerPaths: ['getPaths', ({getPaths}, cbk) => {
133
+ // Exit early when no path has a known introduction node
134
+ if (!getPaths.paths.length) {
135
+ return cbk([503, 'FailedToFindIntroductionNodesOfOfferPaths']);
136
+ }
137
+
138
+ return cbk(null, getPaths.paths);
139
+ }],
140
+
101
141
  // Get a new derived key for the payer id, to derive again from the seed
102
- getPayerKey: ['validate', ({}, cbk) => {
142
+ getPayerKey: ['offerPaths', ({}, cbk) => {
103
143
  return getPublicKey({family: payerKeyFamily, lnd: args.lnd}, cbk);
104
144
  }],
105
145
 
106
- // Make the unsigned invoice request for the offer
146
+ // Make the unsigned invoice request for the offer with the payer id
107
147
  unsigned: [
108
148
  'getNetwork',
109
149
  'getPayerKey',
@@ -150,25 +190,6 @@ module.exports = (args, cbk) => {
150
190
  }
151
191
  }],
152
192
 
153
- // Find the introduction node of each offer path, to not reply from it
154
- getPaths: ['unsigned', ({unsigned}, cbk) => {
155
- return resolvePathIntroductions({
156
- lnd: args.lnd,
157
- paths: unsigned.paths,
158
- },
159
- cbk);
160
- }],
161
-
162
- // The offer paths to send the request over
163
- offerPaths: ['getPaths', ({getPaths}, cbk) => {
164
- // Exit early when no path has a known introduction node
165
- if (!getPaths.paths.length) {
166
- return cbk([503, 'FailedToFindIntroductionNodesOfOfferPaths']);
167
- }
168
-
169
- return cbk(null, getPaths.paths);
170
- }],
171
-
172
193
  // Send the request over each path in turn until the recipient answers
173
194
  getReplies: [
174
195
  'getPeers',
@@ -7,13 +7,18 @@ const {returnResult} = require('asyncjs-util');
7
7
 
8
8
  const {resolvePathIntroductions} = require('./../network');
9
9
 
10
+ const hasBit = (features, bit) => !!features.find(n => n.bit === bit);
10
11
  const isMtokens = n => typeof n === 'string' && /^\d+$/.test(n);
12
+ const requiredMppBit = 16;
11
13
 
12
14
  /** Pay a BOLT 12 invoice over its blinded payment paths
13
15
 
14
16
  The invoice is not checked against an offer. Pay an invoice from
15
17
  `getOfferInvoice`, which checks that it is for the offer and amount asked.
16
18
 
19
+ The invoice is paid over one route, so an invoice that requires multi-part
20
+ payment is not supported.
21
+
17
22
  {
18
23
  invoice: <BOLT 12 Invoice String>
19
24
  lnd: <Authenticated LND API Object>
@@ -71,6 +76,11 @@ module.exports = (args, cbk) => {
71
76
  return cbk([400, 'ExpectedInvoiceForNodeNetworkToPay']);
72
77
  }
73
78
 
79
+ // BOLT 12 pays over multiple paths when the invoice requires MPP
80
+ if (hasBit(details.features, requiredMppBit)) {
81
+ return cbk([400, 'UnsupportedMultiPartPaymentRequiredToPayInvoice']);
82
+ }
83
+
74
84
  if (!details.paths.length) {
75
85
  return cbk([400, 'ExpectedPaymentPathsInInvoiceToPay']);
76
86
  }
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.0.0"
76
+ "version": "10.1.0"
77
77
  }
@@ -28,6 +28,7 @@ const bufferAsHex = buffer => Buffer.from(buffer).toString('hex');
28
28
  const isHex = n => typeof n === 'string' && /^([0-9a-f]{2})+$/i.test(n);
29
29
  const isLimit = n => n === undefined || (isSafeInteger(n) && n > 0);
30
30
  const isPathId = n => isHex(n) && n.length / 2 >= minPathIdBytes;
31
+ const isPublicKey = n => typeof n === 'string' && /^[0-9A-F]{66}$/i.test(n);
31
32
  const isSequence = n => n === undefined || (isSafeInteger(n) && n >= 0);
32
33
  const isServiceId = n => isHex(n) && n.length / 2 === serviceIdBytes;
33
34
 
@@ -38,8 +39,12 @@ const isServiceId = n => isHex(n) && n.length / 2 === serviceIdBytes;
38
39
 
39
40
  The offer has blinded paths to self. Give `paths` made to self, each with
40
41
  the `id` given to self in its final hop, at least 16 random bytes kept
41
- private, to keep the node private. Otherwise a path that starts at self is
42
- created. Keep the offer small, with one path, so that its invoices fit in
42
+ private, to keep the node private. Otherwise a path is created that starts
43
+ at a connected `introduction` peer, or at self when none is given. A node
44
+ with only private channels gives an `introduction` peer that has public
45
+ channels and relays onion messages, since BOLT 12 requires paths from
46
+ publicly reachable nodes in the offer of a node connected only by private
47
+ channels. Keep the offer small, with one path, so that its invoices fit in
43
48
  onion messages of the regular size.
44
49
 
45
50
  The offer has its own issuer id, a derived key that is not linked to the
@@ -63,6 +68,7 @@ const isServiceId = n => isHex(n) && n.length / 2 === serviceIdBytes;
63
68
 
64
69
  {
65
70
  [description]: <Offer Description String>
71
+ [introduction]: <Introduction Peer Public Key Hex String>
66
72
  [issuer]: <Issuer Identifier String>
67
73
  [issuer_record]: <Use Issuer Id of Support Record Hex String>
68
74
  lnd: <Authenticated LND API Object>
@@ -126,6 +132,15 @@ module.exports = (args, cbk) => {
126
132
  return cbk([400, 'ExpectedArrayOfPathsForSupportOffer']);
127
133
  }
128
134
 
135
+ if (!!args.introduction && !isPublicKey(args.introduction)) {
136
+ return cbk([400, 'ExpectedIntroductionPublicKeyForSupportOffer']);
137
+ }
138
+
139
+ // An introduction peer starts a created path, not a given path
140
+ if (!!args.introduction && !!args.paths) {
141
+ return cbk([400, 'ExpectedEitherIntroductionPeerOrGivenPaths']);
142
+ }
143
+
129
144
  if (!!args.suggested_mtokens && !!args.suggested_amounts) {
130
145
  return cbk([400, 'ExpectedEitherSuggestedMtokensOrAmounts']);
131
146
  }
@@ -275,7 +290,11 @@ module.exports = (args, cbk) => {
275
290
  return cbk();
276
291
  }
277
292
 
278
- return createNetworkPath({lnd: args.lnd}, cbk);
293
+ return createNetworkPath({
294
+ introduction: args.introduction,
295
+ lnd: args.lnd,
296
+ },
297
+ cbk);
279
298
  }],
280
299
 
281
300
  // Create the offer