paid-services 9.2.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 +18 -0
- package/offers/check_offer_invoice_reply.js +47 -1
- package/offers/constants.json +1 -0
- package/offers/get_offer_invoice.js +149 -80
- package/offers/invoice_request_for_offer.js +33 -46
- package/offers/issuer_path.js +28 -0
- package/offers/pay_offer.js +8 -4
- package/offers/request_offer_invoice.js +159 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
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
|
+
|
|
17
|
+
## Version 9.3.0
|
|
18
|
+
|
|
19
|
+
- `getOfferInvoice`: Add `description` and `paths` to show and pay the invoice
|
|
20
|
+
|
|
3
21
|
## Version 9.2.0
|
|
4
22
|
|
|
5
23
|
- `getOfferInvoice`: Add method to get an invoice for a BOLT 12 offer
|
|
@@ -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,13 +34,37 @@ 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>
|
|
45
|
+
paths: [{
|
|
46
|
+
base_fee_mtokens: <Accumulated Base Fee Millitokens String>
|
|
47
|
+
cltv_delta: <Accumulated CLTV Expiry Delta Number>
|
|
48
|
+
features: [{
|
|
49
|
+
bit: <BOLT 09 Feature Bit Number>
|
|
50
|
+
is_required: <Feature Support is Required To Pay Bool>
|
|
51
|
+
type: <Feature Type String>
|
|
52
|
+
}]
|
|
53
|
+
fee_rate: <Accumulated Fee Rate Millitokens Per Million Number>
|
|
54
|
+
hops: [{
|
|
55
|
+
encrypted_data: <Encrypted Recipient Data Hex String>
|
|
56
|
+
relay_key: <Blinded Relaying Node Public Key Hex String>
|
|
57
|
+
}]
|
|
58
|
+
[introduction_edge]: <Introduction Node Edge Format Channel Id String>
|
|
59
|
+
[introduction_node]: <Introduction Node Public Key Hex String>
|
|
60
|
+
key: <First Hop Path Key Public Key Hex String>
|
|
61
|
+
max_htlc_mtokens: <Maximum HTLC Millitokens String>
|
|
62
|
+
min_htlc_mtokens: <Minimum HTLC Millitokens String>
|
|
63
|
+
}]
|
|
30
64
|
}
|
|
31
65
|
}
|
|
32
66
|
*/
|
|
33
|
-
module.exports = ({reply, request}) => {
|
|
67
|
+
module.exports = ({path, reply, request}) => {
|
|
34
68
|
// Exit early when the recipient sent back an invoice error
|
|
35
69
|
if (reply.type === typeInvoiceError) {
|
|
36
70
|
try {
|
|
@@ -59,6 +93,16 @@ module.exports = ({reply, request}) => {
|
|
|
59
93
|
request: request.request,
|
|
60
94
|
});
|
|
61
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
|
+
|
|
62
106
|
if (invoice.is_expired) {
|
|
63
107
|
return {error: [503, 'ExpectedUnexpiredInvoiceForOffer']};
|
|
64
108
|
}
|
|
@@ -74,9 +118,11 @@ module.exports = ({reply, request}) => {
|
|
|
74
118
|
return {
|
|
75
119
|
invoice: {
|
|
76
120
|
expires_at: invoice.expires_at,
|
|
121
|
+
features: invoice.features,
|
|
77
122
|
id: invoice.id,
|
|
78
123
|
invoice: invoice.invoice,
|
|
79
124
|
mtokens: invoice.mtokens,
|
|
125
|
+
paths: invoice.paths,
|
|
80
126
|
},
|
|
81
127
|
};
|
|
82
128
|
};
|
package/offers/constants.json
CHANGED
|
@@ -1,33 +1,38 @@
|
|
|
1
1
|
const asyncAuto = require('async/auto');
|
|
2
|
-
const
|
|
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
|
-
const checkOfferInvoiceReply = require('./check_offer_invoice_reply');
|
|
8
10
|
const invoiceRequestForOffer = require('./invoice_request_for_offer');
|
|
9
11
|
const offerInvoiceFailure = require('./offer_invoice_failure');
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
+
const {payerKeyFamily} = require('./constants');
|
|
13
|
+
const requestOfferInvoice = require('./request_offer_invoice');
|
|
12
14
|
const {resolvePathIntroductions} = require('./../network');
|
|
13
|
-
const {typeInvoice} = require('./constants');
|
|
14
|
-
const {typeInvoiceError} = require('./constants');
|
|
15
|
-
const {typeInvoiceRequest} = require('./constants');
|
|
16
15
|
|
|
17
16
|
const {isSafeInteger} = Number;
|
|
18
17
|
const defaultTimeoutMs = 30000;
|
|
19
18
|
const isTimeout = n => isSafeInteger(n) && n > 0;
|
|
19
|
+
const typeSchnorr = 'schnorr';
|
|
20
20
|
|
|
21
21
|
/** Get an invoice for a BOLT 12 offer
|
|
22
22
|
|
|
23
23
|
`mtokens` defaults to the offer amount, and is required when the offer has
|
|
24
|
-
no amount
|
|
25
|
-
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.
|
|
26
26
|
|
|
27
27
|
The request is sent over each path of the offer in turn, waiting `timeout`
|
|
28
28
|
for each, until a path gives back an invoice, or the recipient replies
|
|
29
|
-
with an invoice error, which is returned.
|
|
30
|
-
|
|
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.
|
|
31
36
|
|
|
32
37
|
{
|
|
33
38
|
[bip353_name]: <BIP 353 Human Readable Name Offer Was Found With String>
|
|
@@ -40,12 +45,31 @@ const isTimeout = n => isSafeInteger(n) && n > 0;
|
|
|
40
45
|
|
|
41
46
|
@returns via cbk or Promise
|
|
42
47
|
{
|
|
48
|
+
[description]: <Offer Description String>
|
|
43
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
|
+
}]
|
|
44
55
|
id: <Payment Hash Hex String>
|
|
45
56
|
invoice: <BOLT 12 Invoice String>
|
|
46
57
|
mtokens: <Invoice Amount Millitokens String>
|
|
58
|
+
paths: [{
|
|
59
|
+
base_fee_mtokens: <Accumulated Base Fee Millitokens String>
|
|
60
|
+
cltv_delta: <Accumulated CLTV Expiry Delta Number>
|
|
61
|
+
fee_rate: <Accumulated Fee Rate Millitokens Per Million Number>
|
|
62
|
+
hops: [{
|
|
63
|
+
encrypted_data: <Encrypted Recipient Data Hex String>
|
|
64
|
+
relay_key: <Blinded Relaying Node Public Key Hex String>
|
|
65
|
+
}]
|
|
66
|
+
introduction_node: <Introduction Node Public Key Hex String>
|
|
67
|
+
key: <First Hop Path Key Public Key Hex String>
|
|
68
|
+
max_htlc_mtokens: <Maximum HTLC Millitokens String>
|
|
69
|
+
min_htlc_mtokens: <Minimum HTLC Millitokens String>
|
|
70
|
+
}]
|
|
47
71
|
payer_id: <Payer Public Key Hex String>
|
|
48
|
-
|
|
72
|
+
payer_key_index: <Payer Id Key Index Number>
|
|
49
73
|
}
|
|
50
74
|
*/
|
|
51
75
|
module.exports = (args, cbk) => {
|
|
@@ -74,14 +98,24 @@ module.exports = (args, cbk) => {
|
|
|
74
98
|
// Get the connected peers to start the reply path at one of them
|
|
75
99
|
getPeers: ['validate', ({}, cbk) => getPeers({lnd: args.lnd}, cbk)],
|
|
76
100
|
|
|
77
|
-
//
|
|
78
|
-
|
|
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
|
+
{
|
|
79
112
|
try {
|
|
80
113
|
return cbk(null, invoiceRequestForOffer({
|
|
81
114
|
bip353_name: args.bip353_name,
|
|
82
115
|
mtokens: args.mtokens,
|
|
83
116
|
network: getNetwork.bitcoinjs,
|
|
84
117
|
offer: args.offer,
|
|
118
|
+
payer_id: getPayerKey.public_key,
|
|
85
119
|
payer_note: args.payer_note,
|
|
86
120
|
}));
|
|
87
121
|
} catch (err) {
|
|
@@ -89,103 +123,138 @@ module.exports = (args, cbk) => {
|
|
|
89
123
|
}
|
|
90
124
|
}],
|
|
91
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
|
+
|
|
92
153
|
// Find the introduction node of each offer path, to not reply from it
|
|
93
|
-
getPaths: ['
|
|
154
|
+
getPaths: ['unsigned', ({unsigned}, cbk) => {
|
|
94
155
|
return resolvePathIntroductions({
|
|
95
156
|
lnd: args.lnd,
|
|
96
|
-
paths:
|
|
157
|
+
paths: unsigned.paths,
|
|
97
158
|
},
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
return cbk(err);
|
|
101
|
-
}
|
|
159
|
+
cbk);
|
|
160
|
+
}],
|
|
102
161
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
+
}
|
|
107
168
|
|
|
108
|
-
|
|
109
|
-
});
|
|
169
|
+
return cbk(null, getPaths.paths);
|
|
110
170
|
}],
|
|
111
171
|
|
|
112
|
-
// Send the request over each path in turn until
|
|
113
|
-
|
|
114
|
-
'getPaths',
|
|
172
|
+
// Send the request over each path in turn until the recipient answers
|
|
173
|
+
getReplies: [
|
|
115
174
|
'getPeers',
|
|
175
|
+
'offerPaths',
|
|
116
176
|
'request',
|
|
117
|
-
({
|
|
177
|
+
({getPeers, offerPaths, request}, cbk) =>
|
|
118
178
|
{
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
// Make a task to send the request over a path and check the reply
|
|
122
|
-
const requestOver = path => cbk => {
|
|
123
|
-
const {introduction} = replyIntroduction({
|
|
124
|
-
path,
|
|
125
|
-
peers: getPeers.peers,
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
return requestNetworkReply({
|
|
129
|
-
introduction,
|
|
179
|
+
return asyncReduce(offerPaths, [], (replies, path, cbk) => {
|
|
180
|
+
return requestOfferInvoice({
|
|
130
181
|
path,
|
|
182
|
+
replies,
|
|
183
|
+
request,
|
|
131
184
|
lnd: args.lnd,
|
|
132
|
-
|
|
185
|
+
peers: getPeers.peers,
|
|
133
186
|
timeout: args.timeout || defaultTimeoutMs,
|
|
134
|
-
types: [typeInvoice, typeInvoiceError],
|
|
135
187
|
},
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
return cbk(err);
|
|
142
|
-
}
|
|
188
|
+
cbk);
|
|
189
|
+
},
|
|
190
|
+
cbk);
|
|
191
|
+
}],
|
|
143
192
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
});
|
|
193
|
+
// The invoice the recipient answered with
|
|
194
|
+
getInvoice: ['getReplies', ({getReplies}, cbk) => {
|
|
195
|
+
const answer = getReplies.find(n => !!n.error || !!n.invoice);
|
|
148
196
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
return cbk(null, {error});
|
|
153
|
-
}
|
|
197
|
+
// Exit early when no path gave back an answer from the recipient
|
|
198
|
+
if (!answer) {
|
|
199
|
+
const failures = getReplies.filter(n => !!n.err);
|
|
154
200
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
failures.push({err: error, is_reply: true});
|
|
201
|
+
return cbk(offerInvoiceFailure({failures}).error);
|
|
202
|
+
}
|
|
158
203
|
|
|
159
|
-
|
|
160
|
-
|
|
204
|
+
// Exit early when the recipient said why there is no invoice
|
|
205
|
+
if (!!answer.error) {
|
|
206
|
+
return cbk(answer.error);
|
|
207
|
+
}
|
|
161
208
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
};
|
|
209
|
+
return cbk(null, answer.invoice);
|
|
210
|
+
}],
|
|
165
211
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
212
|
+
// Find the introduction node of each invoice path, to pay over it
|
|
213
|
+
getPaymentPaths: ['getInvoice', ({getInvoice}, cbk) => {
|
|
214
|
+
return resolvePathIntroductions({
|
|
215
|
+
lnd: args.lnd,
|
|
216
|
+
paths: getInvoice.paths,
|
|
217
|
+
},
|
|
218
|
+
cbk);
|
|
219
|
+
}],
|
|
170
220
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
221
|
+
// The payment paths to pay the invoice over
|
|
222
|
+
paymentPaths: ['getPaymentPaths', ({getPaymentPaths}, cbk) => {
|
|
223
|
+
// Exit early when no payment path has a known introduction node
|
|
224
|
+
if (!getPaymentPaths.paths.length) {
|
|
225
|
+
return cbk([503, 'FailedToFindStartOfPaymentPathsForOfferInvoice']);
|
|
226
|
+
}
|
|
175
227
|
|
|
176
|
-
|
|
177
|
-
|
|
228
|
+
return cbk(null, getPaymentPaths.paths.map(path => ({
|
|
229
|
+
base_fee_mtokens: path.base_fee_mtokens,
|
|
230
|
+
cltv_delta: path.cltv_delta,
|
|
231
|
+
fee_rate: path.fee_rate,
|
|
232
|
+
hops: path.hops,
|
|
233
|
+
introduction_node: path.introduction_node,
|
|
234
|
+
key: path.key,
|
|
235
|
+
max_htlc_mtokens: path.max_htlc_mtokens,
|
|
236
|
+
min_htlc_mtokens: path.min_htlc_mtokens,
|
|
237
|
+
})));
|
|
178
238
|
}],
|
|
179
239
|
|
|
180
240
|
// The invoice for the request
|
|
181
|
-
invoice: [
|
|
241
|
+
invoice: [
|
|
242
|
+
'getInvoice',
|
|
243
|
+
'getPayerKey',
|
|
244
|
+
'paymentPaths',
|
|
245
|
+
'unsigned',
|
|
246
|
+
({getInvoice, getPayerKey, paymentPaths, unsigned}, cbk) =>
|
|
247
|
+
{
|
|
182
248
|
return cbk(null, {
|
|
249
|
+
description: unsigned.description,
|
|
183
250
|
expires_at: getInvoice.expires_at,
|
|
251
|
+
features: getInvoice.features,
|
|
184
252
|
id: getInvoice.id,
|
|
185
253
|
invoice: getInvoice.invoice,
|
|
186
254
|
mtokens: getInvoice.mtokens,
|
|
187
|
-
|
|
188
|
-
|
|
255
|
+
paths: paymentPaths,
|
|
256
|
+
payer_id: getPayerKey.public_key,
|
|
257
|
+
payer_key_index: getPayerKey.index,
|
|
189
258
|
});
|
|
190
259
|
}],
|
|
191
260
|
},
|
|
@@ -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
|
|
@@ -32,7 +39,8 @@ const namePattern = /^[0-9a-zA-Z\-_.]+@[0-9a-zA-Z\-_.]+$/;
|
|
|
32
39
|
|
|
33
40
|
@returns
|
|
34
41
|
{
|
|
35
|
-
|
|
42
|
+
[description]: <Offer Description String>
|
|
43
|
+
encoded: <Unsigned Invoice Request TLV Stream Hex String>
|
|
36
44
|
mtokens: <Requested Millitokens String>
|
|
37
45
|
paths: [{
|
|
38
46
|
hops: [{
|
|
@@ -43,9 +51,8 @@ const namePattern = /^[0-9a-zA-Z\-_.]+@[0-9a-zA-Z\-_.]+$/;
|
|
|
43
51
|
[introduction_node]: <Introduction Node Public Key Hex String>
|
|
44
52
|
key: <First Hop Path Key Public Key Hex String>
|
|
45
53
|
}]
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
request: <BOLT 12 Invoice Request String>
|
|
54
|
+
root: <Merkle Root Hash To Sign Hex String>
|
|
55
|
+
tag: <Signature Tagged Hash Tag String>
|
|
49
56
|
}
|
|
50
57
|
*/
|
|
51
58
|
module.exports = args => {
|
|
@@ -61,13 +68,8 @@ module.exports = args => {
|
|
|
61
68
|
throw new Error('ExpectedNetworkToPayOffer');
|
|
62
69
|
}
|
|
63
70
|
|
|
64
|
-
if (
|
|
65
|
-
throw new Error('
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// A secret that is out of the range of private keys has no public key
|
|
69
|
-
if (args.secret !== undefined && !isPrivate(hexAsBuffer(args.secret))) {
|
|
70
|
-
throw new Error('ExpectedValidPayerSecretKeyToPayOffer');
|
|
71
|
+
if (!isKey(args.payer_id)) {
|
|
72
|
+
throw new Error('ExpectedPayerIdPublicKeyToPayOffer');
|
|
71
73
|
}
|
|
72
74
|
|
|
73
75
|
// Exit early when the offer cannot be read
|
|
@@ -92,14 +94,9 @@ module.exports = args => {
|
|
|
92
94
|
throw new Error('ExpectedOfferWithoutQuantityToPay');
|
|
93
95
|
}
|
|
94
96
|
|
|
95
|
-
//
|
|
96
|
-
if (
|
|
97
|
-
throw new Error('
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// An amount in a currency is converted to millitokens by the payer
|
|
101
|
-
if (!!details.currency && !args.mtokens) {
|
|
102
|
-
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');
|
|
103
100
|
}
|
|
104
101
|
|
|
105
102
|
const amount = args.mtokens || details.mtokens;
|
|
@@ -109,39 +106,29 @@ module.exports = args => {
|
|
|
109
106
|
throw new Error('ExpectedMillitokensToPayOfferWithoutAmount');
|
|
110
107
|
}
|
|
111
108
|
|
|
112
|
-
const hasOfferAmount = !details.currency && !!details.mtokens;
|
|
113
|
-
|
|
114
109
|
// An offer with an amount is paid at least that amount
|
|
115
|
-
if (
|
|
110
|
+
if (!!details.mtokens && BigInt(amount) < BigInt(details.mtokens)) {
|
|
116
111
|
throw new Error('ExpectedAtLeastOfferAmountToPayOffer');
|
|
117
112
|
}
|
|
118
113
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const payerId = pointFromScalar(hexAsBuffer(payerSecret));
|
|
122
|
-
|
|
123
|
-
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})];
|
|
124
116
|
|
|
125
117
|
const unsigned = createUnsignedInvoiceRequest({
|
|
126
118
|
bip353_name: args.bip353_name,
|
|
127
119
|
mtokens: amount,
|
|
128
120
|
network: args.network,
|
|
129
121
|
offer: args.offer,
|
|
130
|
-
payer_id:
|
|
122
|
+
payer_id: args.payer_id,
|
|
131
123
|
payer_note: args.payer_note,
|
|
132
124
|
});
|
|
133
125
|
|
|
134
|
-
const signed = createSignedInvoiceRequest({
|
|
135
|
-
encoded: unsigned.encoded,
|
|
136
|
-
secret: payerSecret,
|
|
137
|
-
});
|
|
138
|
-
|
|
139
126
|
return {
|
|
140
|
-
|
|
127
|
+
paths,
|
|
128
|
+
description: details.description,
|
|
129
|
+
encoded: unsigned.encoded,
|
|
141
130
|
mtokens: amount,
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
payer_secret: payerSecret,
|
|
145
|
-
request: signed.request,
|
|
131
|
+
root: unsigned.root,
|
|
132
|
+
tag: unsigned.tag,
|
|
146
133
|
};
|
|
147
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
|
}],
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
const asyncAuto = require('async/auto');
|
|
2
|
+
const asyncReflect = require('async/reflect');
|
|
3
|
+
const {returnResult} = require('asyncjs-util');
|
|
4
|
+
|
|
5
|
+
const checkOfferInvoiceReply = require('./check_offer_invoice_reply');
|
|
6
|
+
const replyIntroduction = require('./../network/reply_introduction');
|
|
7
|
+
const {requestNetworkReply} = require('./../network');
|
|
8
|
+
const {typeInvoice} = require('./constants');
|
|
9
|
+
const {typeInvoiceError} = require('./constants');
|
|
10
|
+
const {typeInvoiceRequest} = require('./constants');
|
|
11
|
+
|
|
12
|
+
const {isArray} = Array;
|
|
13
|
+
const isAnswered = replies => replies.some(n => !!n.error || !!n.invoice);
|
|
14
|
+
|
|
15
|
+
/** Send an invoice request over an offer path and add the reply to replies
|
|
16
|
+
|
|
17
|
+
No request is sent when the recipient already answered over another path,
|
|
18
|
+
with an invoice, or with an invoice error that says why there is no invoice.
|
|
19
|
+
|
|
20
|
+
A path that gives no reply, or a reply that is not a good invoice, is added
|
|
21
|
+
as a failure, so that the request can be sent over the next path.
|
|
22
|
+
|
|
23
|
+
{
|
|
24
|
+
lnd: <Authenticated LND API Object>
|
|
25
|
+
path: {
|
|
26
|
+
hops: [{
|
|
27
|
+
encrypted_data: <Encrypted Recipient Data Hex String>
|
|
28
|
+
relay_key: <Blinded Relaying Node Public Key Hex String>
|
|
29
|
+
}]
|
|
30
|
+
introduction_node: <Introduction Node Public Key Hex String>
|
|
31
|
+
key: <First Hop Path Key Public Key Hex String>
|
|
32
|
+
}
|
|
33
|
+
peers: [{
|
|
34
|
+
features: [{
|
|
35
|
+
bit: <BOLT 09 Feature Bit Number>
|
|
36
|
+
}]
|
|
37
|
+
public_key: <Peer Public Key Hex String>
|
|
38
|
+
}]
|
|
39
|
+
replies: [<Reply Object>]
|
|
40
|
+
request: {
|
|
41
|
+
encoded: <Signed Invoice Request TLV Stream Hex String>
|
|
42
|
+
mtokens: <Requested Millitokens String>
|
|
43
|
+
request: <BOLT 12 Invoice Request String>
|
|
44
|
+
}
|
|
45
|
+
timeout: <Wait For Reply Milliseconds Number>
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@returns via cbk or Promise
|
|
49
|
+
[{
|
|
50
|
+
[err]: [<Failure Code Number>, <Failure Message String>, <Details Object>]
|
|
51
|
+
[error]: [<Error Code Number>, <Error Message String>, <Details Object>]
|
|
52
|
+
[invoice]: {
|
|
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
|
+
}]
|
|
59
|
+
id: <Payment Hash Hex String>
|
|
60
|
+
invoice: <BOLT 12 Invoice String>
|
|
61
|
+
mtokens: <Invoice Amount Millitokens String>
|
|
62
|
+
paths: [<Invoice Payment Path Object>]
|
|
63
|
+
}
|
|
64
|
+
[is_reply]: <Failure Is a Recipient Reply Bool>
|
|
65
|
+
}]
|
|
66
|
+
*/
|
|
67
|
+
module.exports = (args, cbk) => {
|
|
68
|
+
return new Promise((resolve, reject) => {
|
|
69
|
+
return asyncAuto({
|
|
70
|
+
// Check arguments
|
|
71
|
+
validate: cbk => {
|
|
72
|
+
if (!args.lnd) {
|
|
73
|
+
return cbk([400, 'ExpectedLndToRequestOfferInvoice']);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!args.path) {
|
|
77
|
+
return cbk([400, 'ExpectedOfferPathToRequestOfferInvoice']);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!isArray(args.peers)) {
|
|
81
|
+
return cbk([400, 'ExpectedPeersToRequestOfferInvoice']);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!isArray(args.replies)) {
|
|
85
|
+
return cbk([400, 'ExpectedRepliesToRequestOfferInvoice']);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!args.request) {
|
|
89
|
+
return cbk([400, 'ExpectedInvoiceRequestToRequestOfferInvoice']);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!args.timeout) {
|
|
93
|
+
return cbk([400, 'ExpectedTimeoutToRequestOfferInvoice']);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return cbk();
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
// Send the request over the path, unless the recipient already answered
|
|
100
|
+
getReply: ['validate', asyncReflect(({}, cbk) => {
|
|
101
|
+
// Exit early when there is no need to send the request again
|
|
102
|
+
if (isAnswered(args.replies)) {
|
|
103
|
+
return cbk();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const {introduction} = replyIntroduction({
|
|
107
|
+
path: args.path,
|
|
108
|
+
peers: args.peers,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
return requestNetworkReply({
|
|
112
|
+
introduction,
|
|
113
|
+
lnd: args.lnd,
|
|
114
|
+
message: {type: typeInvoiceRequest, value: args.request.encoded},
|
|
115
|
+
path: args.path,
|
|
116
|
+
timeout: args.timeout,
|
|
117
|
+
types: [typeInvoice, typeInvoiceError],
|
|
118
|
+
},
|
|
119
|
+
cbk);
|
|
120
|
+
})],
|
|
121
|
+
|
|
122
|
+
// Add the reply over the path to the replies
|
|
123
|
+
replies: ['getReply', ({getReply}, cbk) => {
|
|
124
|
+
// Exit early when the request was not sent
|
|
125
|
+
if (!getReply.error && !getReply.value) {
|
|
126
|
+
return cbk(null, args.replies);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Exit early when there was no reply, so the next path is tried
|
|
130
|
+
if (!!getReply.error) {
|
|
131
|
+
return cbk(null, [].concat(args.replies, {err: getReply.error}));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const {reply} = getReply.value;
|
|
135
|
+
|
|
136
|
+
const {error, invoice} = checkOfferInvoiceReply({
|
|
137
|
+
reply,
|
|
138
|
+
path: args.path,
|
|
139
|
+
request: args.request,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// Exit early when the recipient said why there is no invoice
|
|
143
|
+
if (!!error && reply.type === typeInvoiceError) {
|
|
144
|
+
return cbk(null, [].concat(args.replies, {error}));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const failure = {err: error, is_reply: true};
|
|
148
|
+
|
|
149
|
+
// Exit early when the reply is not a good invoice
|
|
150
|
+
if (!!error) {
|
|
151
|
+
return cbk(null, [].concat(args.replies, failure));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return cbk(null, [].concat(args.replies, {invoice}));
|
|
155
|
+
}],
|
|
156
|
+
},
|
|
157
|
+
returnResult({reject, resolve, of: 'replies'}, cbk));
|
|
158
|
+
});
|
|
159
|
+
};
|
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.0.0"
|
|
77
77
|
}
|