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 +8 -0
- package/offers/get_offer_invoice.js +45 -24
- package/offers/pay_offer_invoice.js +10 -0
- package/package.json +1 -1
- package/support/create_support_offer.js +22 -3
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
|
|
34
|
-
|
|
35
|
-
|
|
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: ['
|
|
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.
|
|
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
|
|
42
|
-
|
|
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({
|
|
293
|
+
return createNetworkPath({
|
|
294
|
+
introduction: args.introduction,
|
|
295
|
+
lnd: args.lnd,
|
|
296
|
+
},
|
|
297
|
+
cbk);
|
|
279
298
|
}],
|
|
280
299
|
|
|
281
300
|
// Create the offer
|