paid-services 9.3.0 → 10.0.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,19 @@
1
1
  # Versions
2
2
 
3
+ ## Version 10.0.0
4
+
5
+ - `getOfferInvoice`: Add `features` to show the features of the invoice
6
+ - `getOfferInvoice`: Add support for offers with an issuer id and no paths
7
+ - `getOfferInvoice`: Check that an invoice for an offer without an issuer id
8
+ is signed by the final node of the path that the request was sent over
9
+
10
+ ### Breaking Changes
11
+
12
+ - `getOfferInvoice`: Refuse offers with an amount in a currency
13
+ - `getOfferInvoice`: The payer id is now a key derived by LND in key family
14
+ 808, and `payer_key_index` is returned in place of the removed
15
+ `payer_secret`
16
+
3
17
  ## Version 9.3.0
4
18
 
5
19
  - `getOfferInvoice`: Add `description` and `paths` to show and pay the invoice
@@ -1,5 +1,6 @@
1
1
  const {parseInvoice} = require('invoices');
2
2
  const {parseInvoiceError} = require('invoices');
3
+ const {parseOffer} = require('invoices');
3
4
 
4
5
  const {typeInvoiceError} = require('./constants');
5
6
 
@@ -8,7 +9,16 @@ const {typeInvoiceError} = require('./constants');
8
9
  An invoice error reply from the recipient, or an invoice that is not valid,
9
10
  that has expired, or that is not for the request, is a failed reply
10
11
 
12
+ An invoice for an offer without an issuer id has to be signed by the final
13
+ node of the `path` that the request was sent over, as BOLT 12 requires
14
+
11
15
  {
16
+ path: {
17
+ hops: [{
18
+ encrypted_data: <Encrypted Recipient Data Hex String>
19
+ relay_key: <Blinded Relaying Node Public Key Hex String>
20
+ }]
21
+ }
12
22
  reply: {
13
23
  type: <Reply Message Payload Record Type Number String>
14
24
  value: <Reply Message Payload Hex String>
@@ -24,6 +34,11 @@ const {typeInvoiceError} = require('./constants');
24
34
  [error]: [<Error Code Number>, <Error Message String>, <Details Object>]
25
35
  [invoice]: {
26
36
  expires_at: <Invoice Expires At ISO 8601 Date String>
37
+ features: [{
38
+ bit: <BOLT 09 Feature Bit Number>
39
+ is_required: <Feature Support is Required To Pay Bool>
40
+ type: <Feature Type String>
41
+ }]
27
42
  id: <Payment Hash Hex String>
28
43
  invoice: <BOLT 12 Invoice String>
29
44
  mtokens: <Invoice Amount Millitokens String>
@@ -49,7 +64,7 @@ const {typeInvoiceError} = require('./constants');
49
64
  }
50
65
  }
51
66
  */
52
- module.exports = ({reply, request}) => {
67
+ module.exports = ({path, reply, request}) => {
53
68
  // Exit early when the recipient sent back an invoice error
54
69
  if (reply.type === typeInvoiceError) {
55
70
  try {
@@ -78,6 +93,16 @@ module.exports = ({reply, request}) => {
78
93
  request: request.request,
79
94
  });
80
95
 
96
+ // The offer that the invoice is for is copied into the invoice
97
+ const offer = parseOffer({offer: invoice.offer});
98
+
99
+ // An offer without an issuer id has its invoices signed by the path ends
100
+ const [finalHop] = [].concat(path.hops).reverse();
101
+
102
+ if (!offer.issuer_id && invoice.node_id !== finalHop.relay_key) {
103
+ return {error: [503, 'ExpectedInvoiceSignedByFinalNodeOfOfferPathUsed']};
104
+ }
105
+
81
106
  if (invoice.is_expired) {
82
107
  return {error: [503, 'ExpectedUnexpiredInvoiceForOffer']};
83
108
  }
@@ -93,6 +118,7 @@ module.exports = ({reply, request}) => {
93
118
  return {
94
119
  invoice: {
95
120
  expires_at: invoice.expires_at,
121
+ features: invoice.features,
96
122
  id: invoice.id,
97
123
  invoice: invoice.invoice,
98
124
  mtokens: invoice.mtokens,
@@ -1,4 +1,5 @@
1
1
  {
2
+ "payerKeyFamily": 808,
2
3
  "typeInvoice": "66",
3
4
  "typeInvoiceError": "68",
4
5
  "typeInvoiceRequest": "64"
@@ -1,28 +1,38 @@
1
1
  const asyncAuto = require('async/auto');
2
2
  const asyncReduce = require('async/reduce');
3
+ const {createSignedInvoiceRequest} = require('invoices');
3
4
  const {getNetwork} = require('ln-sync');
4
5
  const {getPeers} = require('ln-service');
6
+ const {getPublicKey} = require('ln-service');
5
7
  const {returnResult} = require('asyncjs-util');
8
+ const {signBytes} = require('ln-service');
6
9
 
7
10
  const invoiceRequestForOffer = require('./invoice_request_for_offer');
8
11
  const offerInvoiceFailure = require('./offer_invoice_failure');
12
+ const {payerKeyFamily} = require('./constants');
9
13
  const requestOfferInvoice = require('./request_offer_invoice');
10
14
  const {resolvePathIntroductions} = require('./../network');
11
15
 
12
16
  const {isSafeInteger} = Number;
13
17
  const defaultTimeoutMs = 30000;
14
18
  const isTimeout = n => isSafeInteger(n) && n > 0;
19
+ const typeSchnorr = 'schnorr';
15
20
 
16
21
  /** Get an invoice for a BOLT 12 offer
17
22
 
18
23
  `mtokens` defaults to the offer amount, and is required when the offer has
19
- no amount, or an amount in a currency. Give the `bip353_name` that the
20
- offer was found with, as BOLT 12 requires.
24
+ no amount. An offer with an amount in a currency is not supported. Give the
25
+ `bip353_name` that the offer was found with, as BOLT 12 requires.
21
26
 
22
27
  The request is sent over each path of the offer in turn, waiting `timeout`
23
28
  for each, until a path gives back an invoice, or the recipient replies
24
- with an invoice error, which is returned. Keep `payer_secret`, the key of
25
- the payer id, to be able to prove that the request was made by the payer.
29
+ with an invoice error, which is returned. An offer without paths has the
30
+ request sent to its issuer id.
31
+
32
+ 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.
26
36
 
27
37
  {
28
38
  [bip353_name]: <BIP 353 Human Readable Name Offer Was Found With String>
@@ -37,6 +47,11 @@ const isTimeout = n => isSafeInteger(n) && n > 0;
37
47
  {
38
48
  [description]: <Offer Description String>
39
49
  expires_at: <Invoice Expires At ISO 8601 Date String>
50
+ features: [{
51
+ bit: <BOLT 09 Feature Bit Number>
52
+ is_required: <Feature Support is Required To Pay Bool>
53
+ type: <Feature Type String>
54
+ }]
40
55
  id: <Payment Hash Hex String>
41
56
  invoice: <BOLT 12 Invoice String>
42
57
  mtokens: <Invoice Amount Millitokens String>
@@ -54,7 +69,7 @@ const isTimeout = n => isSafeInteger(n) && n > 0;
54
69
  min_htlc_mtokens: <Minimum HTLC Millitokens String>
55
70
  }]
56
71
  payer_id: <Payer Public Key Hex String>
57
- payer_secret: <Payer Secret Key Hex String>
72
+ payer_key_index: <Payer Id Key Index Number>
58
73
  }
59
74
  */
60
75
  module.exports = (args, cbk) => {
@@ -83,14 +98,24 @@ module.exports = (args, cbk) => {
83
98
  // Get the connected peers to start the reply path at one of them
84
99
  getPeers: ['validate', ({}, cbk) => getPeers({lnd: args.lnd}, cbk)],
85
100
 
86
- // Make the signed invoice request for the offer
87
- request: ['getNetwork', ({getNetwork}, cbk) => {
101
+ // Get a new derived key for the payer id, to derive again from the seed
102
+ getPayerKey: ['validate', ({}, cbk) => {
103
+ return getPublicKey({family: payerKeyFamily, lnd: args.lnd}, cbk);
104
+ }],
105
+
106
+ // Make the unsigned invoice request for the offer
107
+ unsigned: [
108
+ 'getNetwork',
109
+ 'getPayerKey',
110
+ ({getNetwork, getPayerKey}, cbk) =>
111
+ {
88
112
  try {
89
113
  return cbk(null, invoiceRequestForOffer({
90
114
  bip353_name: args.bip353_name,
91
115
  mtokens: args.mtokens,
92
116
  network: getNetwork.bitcoinjs,
93
117
  offer: args.offer,
118
+ payer_id: getPayerKey.public_key,
94
119
  payer_note: args.payer_note,
95
120
  }));
96
121
  } catch (err) {
@@ -98,11 +123,38 @@ module.exports = (args, cbk) => {
98
123
  }
99
124
  }],
100
125
 
126
+ // Sign the request with the payer id key, which never leaves LND
127
+ sign: ['getPayerKey', 'unsigned', ({getPayerKey, unsigned}, cbk) => {
128
+ return signBytes({
129
+ key_family: payerKeyFamily,
130
+ key_index: getPayerKey.index,
131
+ lnd: args.lnd,
132
+ preimage: unsigned.root,
133
+ tag: unsigned.tag,
134
+ type: typeSchnorr,
135
+ },
136
+ cbk);
137
+ }],
138
+
139
+ // Finish the request with its signature, to send it
140
+ request: ['sign', 'unsigned', ({sign, unsigned}, cbk) => {
141
+ try {
142
+ const {encoded, request} = createSignedInvoiceRequest({
143
+ encoded: unsigned.encoded,
144
+ signature: sign.signature,
145
+ });
146
+
147
+ return cbk(null, {encoded, request, mtokens: unsigned.mtokens});
148
+ } catch (err) {
149
+ return cbk([503, 'FailedToCreateSignedInvoiceRequest', {err}]);
150
+ }
151
+ }],
152
+
101
153
  // Find the introduction node of each offer path, to not reply from it
102
- getPaths: ['request', ({request}, cbk) => {
154
+ getPaths: ['unsigned', ({unsigned}, cbk) => {
103
155
  return resolvePathIntroductions({
104
156
  lnd: args.lnd,
105
- paths: request.paths,
157
+ paths: unsigned.paths,
106
158
  },
107
159
  cbk);
108
160
  }],
@@ -188,19 +240,21 @@ module.exports = (args, cbk) => {
188
240
  // The invoice for the request
189
241
  invoice: [
190
242
  'getInvoice',
243
+ 'getPayerKey',
191
244
  'paymentPaths',
192
- 'request',
193
- ({getInvoice, paymentPaths, request}, cbk) =>
245
+ 'unsigned',
246
+ ({getInvoice, getPayerKey, paymentPaths, unsigned}, cbk) =>
194
247
  {
195
248
  return cbk(null, {
196
- description: request.description,
249
+ description: unsigned.description,
197
250
  expires_at: getInvoice.expires_at,
251
+ features: getInvoice.features,
198
252
  id: getInvoice.id,
199
253
  invoice: getInvoice.invoice,
200
254
  mtokens: getInvoice.mtokens,
201
255
  paths: paymentPaths,
202
- payer_id: request.payer_id,
203
- payer_secret: request.payer_secret,
256
+ payer_id: getPayerKey.public_key,
257
+ payer_key_index: getPayerKey.index,
204
258
  });
205
259
  }],
206
260
  },
@@ -1,30 +1,37 @@
1
- const {createSignedInvoiceRequest} = require('invoices');
2
1
  const {createUnsignedInvoiceRequest} = require('invoices');
3
- const {isPrivate} = require('tiny-secp256k1');
2
+ const {isPoint} = require('tiny-secp256k1');
4
3
  const {parseOffer} = require('invoices');
5
- const {pointFromScalar} = require('tiny-secp256k1');
6
4
 
7
- const randomSecret = require('./random_secret');
5
+ const issuerPath = require('./issuer_path');
8
6
 
9
- const bufferAsHex = buffer => buffer.toString('hex');
10
7
  const hexAsBuffer = hex => Buffer.from(hex, 'hex');
8
+ const isHex = n => typeof n === 'string' && /^([0-9a-f]{2})+$/i.test(n);
9
+ const isKey = n => isHex(n) && n.length === 66 && isPoint(hexAsBuffer(n));
11
10
  const isMtokens = n => typeof n === 'string' && /^[1-9]\d*$/.test(n);
12
11
  const isName = n => typeof n === 'string' && namePattern.test(n);
13
- const isSecret = n => typeof n === 'string' && /^[0-9a-f]{64}$/i.test(n);
14
12
  const namePattern = /^[0-9a-zA-Z\-_.]+@[0-9a-zA-Z\-_.]+$/;
15
13
 
16
- /** Make a signed BOLT 12 invoice request, with a random payer key by default
14
+ /** Make an unsigned BOLT 12 invoice request for an offer, with a payer id
15
+
16
+ `mtokens` defaults to the offer amount, and is required when the offer has
17
+ no amount. An offer with an amount in a currency is not supported.
17
18
 
18
19
  Give the `bip353_name` that the offer was found with, as BOLT 12 requires,
19
20
  so that the recipient knows which name was used
20
21
 
22
+ Sign `root` with `tag` as the tagged hash tag, with the key of `payer_id`,
23
+ and give the signature to `createSignedInvoiceRequest` to finish the request
24
+
25
+ The request is sent over the `paths`: the paths of the offer, or a path to
26
+ the issuer id of an offer that has no paths, as BOLT 12 requires
27
+
21
28
  {
22
29
  [bip353_name]: <BIP 353 Human Readable Name Offer Was Found With String>
23
30
  [mtokens]: <Millitokens to Pay String>
24
31
  network: <Network Name String>
25
32
  offer: <BOLT 12 Offer String>
33
+ payer_id: <Payer Public Key Hex String>
26
34
  [payer_note]: <Payer Note String>
27
- [secret]: <Payer Secret Key Hex String>
28
35
  }
29
36
 
30
37
  @throws
@@ -33,7 +40,7 @@ const namePattern = /^[0-9a-zA-Z\-_.]+@[0-9a-zA-Z\-_.]+$/;
33
40
  @returns
34
41
  {
35
42
  [description]: <Offer Description String>
36
- encoded: <Signed Invoice Request TLV Stream Hex String>
43
+ encoded: <Unsigned Invoice Request TLV Stream Hex String>
37
44
  mtokens: <Requested Millitokens String>
38
45
  paths: [{
39
46
  hops: [{
@@ -44,9 +51,8 @@ const namePattern = /^[0-9a-zA-Z\-_.]+@[0-9a-zA-Z\-_.]+$/;
44
51
  [introduction_node]: <Introduction Node Public Key Hex String>
45
52
  key: <First Hop Path Key Public Key Hex String>
46
53
  }]
47
- payer_id: <Payer Public Key Hex String>
48
- payer_secret: <Payer Secret Key Hex String>
49
- request: <BOLT 12 Invoice Request String>
54
+ root: <Merkle Root Hash To Sign Hex String>
55
+ tag: <Signature Tagged Hash Tag String>
50
56
  }
51
57
  */
52
58
  module.exports = args => {
@@ -62,13 +68,8 @@ module.exports = args => {
62
68
  throw new Error('ExpectedNetworkToPayOffer');
63
69
  }
64
70
 
65
- if (args.secret !== undefined && !isSecret(args.secret)) {
66
- throw new Error('ExpectedPayerSecretKeyToPayOffer');
67
- }
68
-
69
- // A secret that is out of the range of private keys has no public key
70
- if (args.secret !== undefined && !isPrivate(hexAsBuffer(args.secret))) {
71
- throw new Error('ExpectedValidPayerSecretKeyToPayOffer');
71
+ if (!isKey(args.payer_id)) {
72
+ throw new Error('ExpectedPayerIdPublicKeyToPayOffer');
72
73
  }
73
74
 
74
75
  // Exit early when the offer cannot be read
@@ -93,14 +94,9 @@ module.exports = args => {
93
94
  throw new Error('ExpectedOfferWithoutQuantityToPay');
94
95
  }
95
96
 
96
- // Requests are sent over the blinded paths of the offer
97
- if (!details.paths || !details.paths.length) {
98
- throw new Error('ExpectedOfferWithPathsToPay');
99
- }
100
-
101
- // An amount in a currency is converted to millitokens by the payer
102
- if (!!details.currency && !args.mtokens) {
103
- throw new Error('ExpectedMillitokensToPayOfferWithCurrencyAmount');
97
+ // An amount in a currency needs an exchange rate, which is not supported
98
+ if (!!details.currency) {
99
+ throw new Error('UnsupportedCurrencyAmountInOfferToPay');
104
100
  }
105
101
 
106
102
  const amount = args.mtokens || details.mtokens;
@@ -110,40 +106,29 @@ module.exports = args => {
110
106
  throw new Error('ExpectedMillitokensToPayOfferWithoutAmount');
111
107
  }
112
108
 
113
- const hasOfferAmount = !details.currency && !!details.mtokens;
114
-
115
109
  // An offer with an amount is paid at least that amount
116
- if (hasOfferAmount && BigInt(amount) < BigInt(details.mtokens)) {
110
+ if (!!details.mtokens && BigInt(amount) < BigInt(details.mtokens)) {
117
111
  throw new Error('ExpectedAtLeastOfferAmountToPayOffer');
118
112
  }
119
113
 
120
- const payerSecret = args.secret || randomSecret().secret;
121
-
122
- const payerId = pointFromScalar(hexAsBuffer(payerSecret));
123
-
124
- const payer = bufferAsHex(Buffer.from(payerId));
114
+ // A request is sent over the offer paths, or to the issuer id without them
115
+ const paths = details.paths || [issuerPath({id: details.issuer_id})];
125
116
 
126
117
  const unsigned = createUnsignedInvoiceRequest({
127
118
  bip353_name: args.bip353_name,
128
119
  mtokens: amount,
129
120
  network: args.network,
130
121
  offer: args.offer,
131
- payer_id: payer,
122
+ payer_id: args.payer_id,
132
123
  payer_note: args.payer_note,
133
124
  });
134
125
 
135
- const signed = createSignedInvoiceRequest({
136
- encoded: unsigned.encoded,
137
- secret: payerSecret,
138
- });
139
-
140
126
  return {
127
+ paths,
141
128
  description: details.description,
142
- encoded: signed.encoded,
129
+ encoded: unsigned.encoded,
143
130
  mtokens: amount,
144
- paths: details.paths,
145
- payer_id: payer,
146
- payer_secret: payerSecret,
147
- request: signed.request,
131
+ root: unsigned.root,
132
+ tag: unsigned.tag,
148
133
  };
149
134
  };
@@ -0,0 +1,28 @@
1
+ const {blindedPathToNode} = require('bolt04');
2
+
3
+ /** Make a path to send an invoice request to the issuer id of an offer
4
+
5
+ BOLT 12 sends requests for an offer without paths to its issuer id
6
+
7
+ {
8
+ id: <Offer Issuer Id Public Key Hex String>
9
+ }
10
+
11
+ @throws
12
+ <Error>
13
+
14
+ @returns
15
+ {
16
+ hops: [{
17
+ encrypted_data: <Encrypted Recipient Data Hex String>
18
+ relay_key: <Blinded Relaying Node Public Key Hex String>
19
+ }]
20
+ introduction_node: <Introduction Node Public Key Hex String>
21
+ key: <First Hop Path Key Public Key Hex String>
22
+ }
23
+ */
24
+ module.exports = ({id}) => {
25
+ const {key, path} = blindedPathToNode({hops: [id]});
26
+
27
+ return {key, hops: path, introduction_node: id};
28
+ };
@@ -11,8 +11,12 @@ const isTimeout = n => isSafeInteger(n) && n > 0;
11
11
  /** Pay a BOLT 12 offer: `getOfferInvoice` and then `payOfferInvoice`
12
12
 
13
13
  `mtokens` defaults to the offer amount, and is required when the offer has
14
- no amount, or an amount in a currency. Give the `bip353_name` that the
15
- offer was found with, as BOLT 12 requires.
14
+ no amount. An offer with an amount in a currency is not supported. Give the
15
+ `bip353_name` that the offer was found with, as BOLT 12 requires.
16
+
17
+ The `payer_id` is a new key that LND derives in key family 808 at
18
+ `payer_key_index`. Keep the index to be able to prove that the request and
19
+ the payment were made by the payer.
16
20
 
17
21
  {
18
22
  [bip353_name]: <BIP 353 Human Readable Name Offer Was Found With String>
@@ -31,7 +35,7 @@ const isTimeout = n => isSafeInteger(n) && n > 0;
31
35
  invoice: <BOLT 12 Invoice String>
32
36
  mtokens: <Invoice Amount Millitokens String>
33
37
  payer_id: <Payer Public Key Hex String>
34
- payer_secret: <Payer Secret Key Hex String>
38
+ payer_key_index: <Payer Id Key Index Number>
35
39
  secret: <Payment Preimage Hex String>
36
40
  }
37
41
  */
@@ -91,7 +95,7 @@ module.exports = (args, cbk) => {
91
95
  invoice: getInvoice.invoice,
92
96
  mtokens: pay.mtokens,
93
97
  payer_id: getInvoice.payer_id,
94
- payer_secret: getInvoice.payer_secret,
98
+ payer_key_index: getInvoice.payer_key_index,
95
99
  secret: pay.secret,
96
100
  });
97
101
  }],
@@ -51,6 +51,11 @@ const isAnswered = replies => replies.some(n => !!n.error || !!n.invoice);
51
51
  [error]: [<Error Code Number>, <Error Message String>, <Details Object>]
52
52
  [invoice]: {
53
53
  expires_at: <Invoice Expires At ISO 8601 Date String>
54
+ features: [{
55
+ bit: <BOLT 09 Feature Bit Number>
56
+ is_required: <Feature Support is Required To Pay Bool>
57
+ type: <Feature Type String>
58
+ }]
54
59
  id: <Payment Hash Hex String>
55
60
  invoice: <BOLT 12 Invoice String>
56
61
  mtokens: <Invoice Amount Millitokens String>
@@ -130,6 +135,7 @@ module.exports = (args, cbk) => {
130
135
 
131
136
  const {error, invoice} = checkOfferInvoiceReply({
132
137
  reply,
138
+ path: args.path,
133
139
  request: args.request,
134
140
  });
135
141
 
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": "9.3.0"
76
+ "version": "10.0.0"
77
77
  }