paid-services 9.3.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 +22 -0
- package/offers/check_offer_invoice_reply.js +27 -1
- package/offers/constants.json +1 -0
- package/offers/get_offer_invoice.js +89 -14
- package/offers/invoice_request_for_offer.js +31 -46
- package/offers/issuer_path.js +28 -0
- package/offers/pay_offer.js +8 -4
- package/offers/pay_offer_invoice.js +10 -0
- package/offers/request_offer_invoice.js +6 -0
- package/package.json +1 -1
- package/support/create_support_offer.js +22 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
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
|
+
|
|
11
|
+
## Version 10.0.0
|
|
12
|
+
|
|
13
|
+
- `getOfferInvoice`: Add `features` to show the features of the invoice
|
|
14
|
+
- `getOfferInvoice`: Add support for offers with an issuer id and no paths
|
|
15
|
+
- `getOfferInvoice`: Check that an invoice for an offer without an issuer id
|
|
16
|
+
is signed by the final node of the path that the request was sent over
|
|
17
|
+
|
|
18
|
+
### Breaking Changes
|
|
19
|
+
|
|
20
|
+
- `getOfferInvoice`: Refuse offers with an amount in a currency
|
|
21
|
+
- `getOfferInvoice`: The payer id is now a key derived by LND in key family
|
|
22
|
+
808, and `payer_key_index` is returned in place of the removed
|
|
23
|
+
`payer_secret`
|
|
24
|
+
|
|
3
25
|
## Version 9.3.0
|
|
4
26
|
|
|
5
27
|
- `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,
|
package/offers/constants.json
CHANGED
|
@@ -1,28 +1,42 @@
|
|
|
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');
|
|
7
|
+
const {pointFromScalar} = require('tiny-secp256k1');
|
|
5
8
|
const {returnResult} = require('asyncjs-util');
|
|
9
|
+
const {signBytes} = require('ln-service');
|
|
6
10
|
|
|
7
11
|
const invoiceRequestForOffer = require('./invoice_request_for_offer');
|
|
8
12
|
const offerInvoiceFailure = require('./offer_invoice_failure');
|
|
13
|
+
const {payerKeyFamily} = require('./constants');
|
|
9
14
|
const requestOfferInvoice = require('./request_offer_invoice');
|
|
10
15
|
const {resolvePathIntroductions} = require('./../network');
|
|
11
16
|
|
|
12
17
|
const {isSafeInteger} = Number;
|
|
18
|
+
const bufferAsHex = buffer => Buffer.from(buffer).toString('hex');
|
|
13
19
|
const defaultTimeoutMs = 30000;
|
|
14
20
|
const isTimeout = n => isSafeInteger(n) && n > 0;
|
|
21
|
+
const placeholderPayerId = bufferAsHex(pointFromScalar(Buffer.alloc(32, 1)));
|
|
22
|
+
const typeSchnorr = 'schnorr';
|
|
15
23
|
|
|
16
24
|
/** Get an invoice for a BOLT 12 offer
|
|
17
25
|
|
|
18
26
|
`mtokens` defaults to the offer amount, and is required when the offer has
|
|
19
|
-
no amount
|
|
20
|
-
offer was found with, as BOLT 12 requires.
|
|
27
|
+
no amount. An offer with an amount in a currency is not supported. Give the
|
|
28
|
+
`bip353_name` that the offer was found with, as BOLT 12 requires.
|
|
21
29
|
|
|
22
30
|
The request is sent over each path of the offer in turn, waiting `timeout`
|
|
23
31
|
for each, until a path gives back an invoice, or the recipient replies
|
|
24
|
-
with an invoice error, which is returned.
|
|
25
|
-
|
|
32
|
+
with an invoice error, which is returned. An offer without paths has the
|
|
33
|
+
request sent to its issuer id.
|
|
34
|
+
|
|
35
|
+
The request is signed by the `payer_id`, a new key that LND derives in key
|
|
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.
|
|
26
40
|
|
|
27
41
|
{
|
|
28
42
|
[bip353_name]: <BIP 353 Human Readable Name Offer Was Found With String>
|
|
@@ -37,6 +51,11 @@ const isTimeout = n => isSafeInteger(n) && n > 0;
|
|
|
37
51
|
{
|
|
38
52
|
[description]: <Offer Description String>
|
|
39
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
|
+
}]
|
|
40
59
|
id: <Payment Hash Hex String>
|
|
41
60
|
invoice: <BOLT 12 Invoice String>
|
|
42
61
|
mtokens: <Invoice Amount Millitokens String>
|
|
@@ -54,7 +73,7 @@ const isTimeout = n => isSafeInteger(n) && n > 0;
|
|
|
54
73
|
min_htlc_mtokens: <Minimum HTLC Millitokens String>
|
|
55
74
|
}]
|
|
56
75
|
payer_id: <Payer Public Key Hex String>
|
|
57
|
-
|
|
76
|
+
payer_key_index: <Payer Id Key Index Number>
|
|
58
77
|
}
|
|
59
78
|
*/
|
|
60
79
|
module.exports = (args, cbk) => {
|
|
@@ -83,14 +102,16 @@ module.exports = (args, cbk) => {
|
|
|
83
102
|
// Get the connected peers to start the reply path at one of them
|
|
84
103
|
getPeers: ['validate', ({}, cbk) => getPeers({lnd: args.lnd}, cbk)],
|
|
85
104
|
|
|
86
|
-
// Make the
|
|
87
|
-
|
|
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) => {
|
|
88
108
|
try {
|
|
89
109
|
return cbk(null, invoiceRequestForOffer({
|
|
90
110
|
bip353_name: args.bip353_name,
|
|
91
111
|
mtokens: args.mtokens,
|
|
92
112
|
network: getNetwork.bitcoinjs,
|
|
93
113
|
offer: args.offer,
|
|
114
|
+
payer_id: placeholderPayerId,
|
|
94
115
|
payer_note: args.payer_note,
|
|
95
116
|
}));
|
|
96
117
|
} catch (err) {
|
|
@@ -99,10 +120,10 @@ module.exports = (args, cbk) => {
|
|
|
99
120
|
}],
|
|
100
121
|
|
|
101
122
|
// Find the introduction node of each offer path, to not reply from it
|
|
102
|
-
getPaths: ['
|
|
123
|
+
getPaths: ['checkRequest', ({checkRequest}, cbk) => {
|
|
103
124
|
return resolvePathIntroductions({
|
|
104
125
|
lnd: args.lnd,
|
|
105
|
-
paths:
|
|
126
|
+
paths: checkRequest.paths,
|
|
106
127
|
},
|
|
107
128
|
cbk);
|
|
108
129
|
}],
|
|
@@ -117,6 +138,58 @@ module.exports = (args, cbk) => {
|
|
|
117
138
|
return cbk(null, getPaths.paths);
|
|
118
139
|
}],
|
|
119
140
|
|
|
141
|
+
// Get a new derived key for the payer id, to derive again from the seed
|
|
142
|
+
getPayerKey: ['offerPaths', ({}, cbk) => {
|
|
143
|
+
return getPublicKey({family: payerKeyFamily, lnd: args.lnd}, cbk);
|
|
144
|
+
}],
|
|
145
|
+
|
|
146
|
+
// Make the unsigned invoice request for the offer with the payer id
|
|
147
|
+
unsigned: [
|
|
148
|
+
'getNetwork',
|
|
149
|
+
'getPayerKey',
|
|
150
|
+
({getNetwork, getPayerKey}, cbk) =>
|
|
151
|
+
{
|
|
152
|
+
try {
|
|
153
|
+
return cbk(null, invoiceRequestForOffer({
|
|
154
|
+
bip353_name: args.bip353_name,
|
|
155
|
+
mtokens: args.mtokens,
|
|
156
|
+
network: getNetwork.bitcoinjs,
|
|
157
|
+
offer: args.offer,
|
|
158
|
+
payer_id: getPayerKey.public_key,
|
|
159
|
+
payer_note: args.payer_note,
|
|
160
|
+
}));
|
|
161
|
+
} catch (err) {
|
|
162
|
+
return cbk([400, err.message]);
|
|
163
|
+
}
|
|
164
|
+
}],
|
|
165
|
+
|
|
166
|
+
// Sign the request with the payer id key, which never leaves LND
|
|
167
|
+
sign: ['getPayerKey', 'unsigned', ({getPayerKey, unsigned}, cbk) => {
|
|
168
|
+
return signBytes({
|
|
169
|
+
key_family: payerKeyFamily,
|
|
170
|
+
key_index: getPayerKey.index,
|
|
171
|
+
lnd: args.lnd,
|
|
172
|
+
preimage: unsigned.root,
|
|
173
|
+
tag: unsigned.tag,
|
|
174
|
+
type: typeSchnorr,
|
|
175
|
+
},
|
|
176
|
+
cbk);
|
|
177
|
+
}],
|
|
178
|
+
|
|
179
|
+
// Finish the request with its signature, to send it
|
|
180
|
+
request: ['sign', 'unsigned', ({sign, unsigned}, cbk) => {
|
|
181
|
+
try {
|
|
182
|
+
const {encoded, request} = createSignedInvoiceRequest({
|
|
183
|
+
encoded: unsigned.encoded,
|
|
184
|
+
signature: sign.signature,
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
return cbk(null, {encoded, request, mtokens: unsigned.mtokens});
|
|
188
|
+
} catch (err) {
|
|
189
|
+
return cbk([503, 'FailedToCreateSignedInvoiceRequest', {err}]);
|
|
190
|
+
}
|
|
191
|
+
}],
|
|
192
|
+
|
|
120
193
|
// Send the request over each path in turn until the recipient answers
|
|
121
194
|
getReplies: [
|
|
122
195
|
'getPeers',
|
|
@@ -188,19 +261,21 @@ module.exports = (args, cbk) => {
|
|
|
188
261
|
// The invoice for the request
|
|
189
262
|
invoice: [
|
|
190
263
|
'getInvoice',
|
|
264
|
+
'getPayerKey',
|
|
191
265
|
'paymentPaths',
|
|
192
|
-
'
|
|
193
|
-
({getInvoice, paymentPaths,
|
|
266
|
+
'unsigned',
|
|
267
|
+
({getInvoice, getPayerKey, paymentPaths, unsigned}, cbk) =>
|
|
194
268
|
{
|
|
195
269
|
return cbk(null, {
|
|
196
|
-
description:
|
|
270
|
+
description: unsigned.description,
|
|
197
271
|
expires_at: getInvoice.expires_at,
|
|
272
|
+
features: getInvoice.features,
|
|
198
273
|
id: getInvoice.id,
|
|
199
274
|
invoice: getInvoice.invoice,
|
|
200
275
|
mtokens: getInvoice.mtokens,
|
|
201
276
|
paths: paymentPaths,
|
|
202
|
-
payer_id:
|
|
203
|
-
|
|
277
|
+
payer_id: getPayerKey.public_key,
|
|
278
|
+
payer_key_index: getPayerKey.index,
|
|
204
279
|
});
|
|
205
280
|
}],
|
|
206
281
|
},
|
|
@@ -1,30 +1,37 @@
|
|
|
1
|
-
const {createSignedInvoiceRequest} = require('invoices');
|
|
2
1
|
const {createUnsignedInvoiceRequest} = require('invoices');
|
|
3
|
-
const {
|
|
2
|
+
const {isPoint} = require('tiny-secp256k1');
|
|
4
3
|
const {parseOffer} = require('invoices');
|
|
5
|
-
const {pointFromScalar} = require('tiny-secp256k1');
|
|
6
4
|
|
|
7
|
-
const
|
|
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
|
|
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: <
|
|
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
|
-
|
|
48
|
-
|
|
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 (
|
|
66
|
-
throw new Error('
|
|
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
|
-
//
|
|
97
|
-
if (
|
|
98
|
-
throw new Error('
|
|
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 (
|
|
110
|
+
if (!!details.mtokens && BigInt(amount) < BigInt(details.mtokens)) {
|
|
117
111
|
throw new Error('ExpectedAtLeastOfferAmountToPayOffer');
|
|
118
112
|
}
|
|
119
113
|
|
|
120
|
-
|
|
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:
|
|
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:
|
|
129
|
+
encoded: unsigned.encoded,
|
|
143
130
|
mtokens: amount,
|
|
144
|
-
|
|
145
|
-
|
|
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
|
+
};
|
package/offers/pay_offer.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
98
|
+
payer_key_index: getInvoice.payer_key_index,
|
|
95
99
|
secret: pay.secret,
|
|
96
100
|
});
|
|
97
101
|
}],
|
|
@@ -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
|
}
|
|
@@ -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": "
|
|
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
|