balanceofsatoshis 13.7.2 → 13.8.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 +4 -0
- package/bos +7 -0
- package/offchain/create_invoice.js +124 -9
- package/offchain/sign_payment_request.js +74 -10
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
package/bos
CHANGED
|
@@ -1057,24 +1057,31 @@ prog
|
|
|
1057
1057
|
.help('Amount can take m/k variables: 5*m for 5 million, 250*k = 0.0025')
|
|
1058
1058
|
.help('Fiat conversion is supported in amount, N*USD or N*EUR')
|
|
1059
1059
|
.help(`Fiat rate providers: ${priceProviders.join(', ')}`)
|
|
1060
|
+
.help('--virtual invoices cannot be used with payers who probe before pay')
|
|
1061
|
+
.help('Only one --virtual invoice can be active at a time')
|
|
1060
1062
|
.argument('[amount]', 'Amount for invoice', STRING, '0')
|
|
1061
1063
|
.option('--for <description>', 'What is the invoice requesting payment for')
|
|
1062
1064
|
.option('--include-hints', 'Include the default set of hop hint channels')
|
|
1063
1065
|
.option('--node <node_name>', 'Use saved node to create invoice')
|
|
1064
1066
|
.option('--rate-provider <rate_provider>', 'Rate provider', priceProviders)
|
|
1065
1067
|
.option('--select-hints', 'Select hop hints to be added to the request')
|
|
1068
|
+
.option('--virtual', 'Request payment over a virtual channel')
|
|
1069
|
+
.option('--virtual-fee-rate <pm>', 'Fee rate to use on virtual channel', INT)
|
|
1066
1070
|
.action((args, options, logger) => {
|
|
1067
1071
|
return new Promise(async (resolve, reject) => {
|
|
1068
1072
|
try {
|
|
1069
1073
|
return await offchain.createInvoice({
|
|
1074
|
+
logger,
|
|
1070
1075
|
amount: args.amount,
|
|
1071
1076
|
ask: await commands.interrogate({}),
|
|
1072
1077
|
description: options.for,
|
|
1073
1078
|
is_hinting: options.includeHints || undefined,
|
|
1074
1079
|
is_selecting_hops: options.selectHints || undefined,
|
|
1080
|
+
is_virtual: options.virtual || undefined,
|
|
1075
1081
|
lnd: (await lnd.authenticatedLnd({logger, node: options.node})).lnd,
|
|
1076
1082
|
rate_provider: options.rateProvider || undefined,
|
|
1077
1083
|
request: commands.simpleRequest,
|
|
1084
|
+
virtual_fee_rate: options.virtualFeeRate,
|
|
1078
1085
|
},
|
|
1079
1086
|
responses.returnObject({exit, logger, reject, resolve}));
|
|
1080
1087
|
} catch (err) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const asyncAuto = require('async/auto');
|
|
2
2
|
const asyncMap = require('async/map');
|
|
3
|
+
const asyncRetry = require('async/retry');
|
|
3
4
|
const {createInvoice} = require('ln-service');
|
|
4
5
|
const {getChannels} = require('ln-service');
|
|
5
6
|
const {getChannel} = require('ln-service');
|
|
@@ -10,6 +11,7 @@ const {getPrices} = require('@alexbosworth/fiat');
|
|
|
10
11
|
const {parseAmount} = require('ln-accounting');
|
|
11
12
|
const {parsePaymentRequest} = require('ln-service');
|
|
12
13
|
const {returnResult} = require('asyncjs-util');
|
|
14
|
+
const {subscribeToForwardRequests} = require('ln-service');
|
|
13
15
|
|
|
14
16
|
const signPaymentRequest = require('./sign_payment_request');
|
|
15
17
|
|
|
@@ -18,12 +20,15 @@ const defaultFiatRateProvider = 'coinbase';
|
|
|
18
20
|
const defaultInvoiceDescription = '';
|
|
19
21
|
const fiats = ['EUR', 'USD'];
|
|
20
22
|
const hasFiat = n => /(eur|usd)/gim.test(n);
|
|
23
|
+
const interval = 3000;
|
|
21
24
|
const {isArray} = Array;
|
|
22
25
|
const {isInteger} = Number;
|
|
23
26
|
const isNumber = n => !isNaN(n);
|
|
27
|
+
const mtokensAsBigUnit = n => (Number(n / BigInt(1000)) / 1e8).toFixed(8);
|
|
24
28
|
const networks = {btc: 'BTC', btctestnet: 'BTC', btcregtest: 'BTC'};
|
|
25
29
|
const parseRequest = request => parsePaymentRequest({request});
|
|
26
30
|
const rateAsTokens = rate => 1e10 / rate;
|
|
31
|
+
const times = 20 * 60 * 24;
|
|
27
32
|
const tokensAsBigUnit = tokens => (tokens / 1e8).toFixed(8);
|
|
28
33
|
const uniq = arr => Array.from(new Set(arr));
|
|
29
34
|
|
|
@@ -35,6 +40,7 @@ const uniq = arr => Array.from(new Set(arr));
|
|
|
35
40
|
[description]: <Invoice Description String>
|
|
36
41
|
[is_hinting]: <Include Private Channels Bool>
|
|
37
42
|
[is_selecting_hops]: <Is Selecting Hops Bool>
|
|
43
|
+
[is_virtual]: <Is Using Virtual Channel for Invoice Bool>
|
|
38
44
|
lnd: <Authenticated LND API Object>
|
|
39
45
|
[rate_provider]: <Fiat Rate Provider String>
|
|
40
46
|
request: <Request Function>
|
|
@@ -42,8 +48,9 @@ const uniq = arr => Array.from(new Set(arr));
|
|
|
42
48
|
|
|
43
49
|
@returns via cbk or Promise
|
|
44
50
|
{
|
|
45
|
-
|
|
46
|
-
|
|
51
|
+
[is_settled]: <Invoice Was Paid Bool>
|
|
52
|
+
[request]: <BOLT 11 Payment Request String>
|
|
53
|
+
[tokens]: <Invoice Amount Number>
|
|
47
54
|
}
|
|
48
55
|
*/
|
|
49
56
|
module.exports = (args, cbk) => {
|
|
@@ -67,10 +74,22 @@ module.exports = (args, cbk) => {
|
|
|
67
74
|
return cbk([400, 'CannotUseDefaultHintsAndAlsoSelectHints']);
|
|
68
75
|
}
|
|
69
76
|
|
|
77
|
+
if (!!args.is_virtual && !!args.is_hinting) {
|
|
78
|
+
return cbk([400, 'UsingHopHintsIsUnsupportedWithVirtualChannels']);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!!args.is_virtual && !!args.is_selecting_hops) {
|
|
82
|
+
return cbk([400, 'ChoosingHopHintsUnsupportedWithVirtualChannels']);
|
|
83
|
+
}
|
|
84
|
+
|
|
70
85
|
if (!args.lnd) {
|
|
71
86
|
return cbk([400, 'ExpectedAuthenticatedLndToCreateNewInvoice']);
|
|
72
87
|
}
|
|
73
88
|
|
|
89
|
+
if (!args.logger) {
|
|
90
|
+
return cbk([400, 'ExpectedWinstonLoggerObjectToCreateNewInvoice']);
|
|
91
|
+
}
|
|
92
|
+
|
|
74
93
|
if (!args.request) {
|
|
75
94
|
return cbk([400, 'ExpectedRequestFunctionToCreateNewInvoice']);
|
|
76
95
|
}
|
|
@@ -127,7 +146,7 @@ module.exports = (args, cbk) => {
|
|
|
127
146
|
getNetwork: ['validate', ({}, cbk) => getNetwork({lnd: args.lnd}, cbk)],
|
|
128
147
|
|
|
129
148
|
// Get wallet info
|
|
130
|
-
|
|
149
|
+
getId: ['validate', ({}, cbk) => getIdentity({lnd: args.lnd}, cbk)],
|
|
131
150
|
|
|
132
151
|
// Fiat rates
|
|
133
152
|
rates: [
|
|
@@ -185,6 +204,7 @@ module.exports = (args, cbk) => {
|
|
|
185
204
|
return cbk();
|
|
186
205
|
}
|
|
187
206
|
|
|
207
|
+
// Make sure there are some channels to select
|
|
188
208
|
if (!getChannels.channels.length) {
|
|
189
209
|
return cbk([400, 'NoRelevantChannelsToSelectAsHints']);
|
|
190
210
|
}
|
|
@@ -217,6 +237,7 @@ module.exports = (args, cbk) => {
|
|
|
217
237
|
getPolicies: ['selectChannels', ({selectChannels}, cbk) => {
|
|
218
238
|
return asyncMap(selectChannels, (channel, cbk) => {
|
|
219
239
|
return getChannel({id: channel, lnd: args.lnd}, (err, res) => {
|
|
240
|
+
// Exit early when the channel isn't found
|
|
220
241
|
if (isArray(err) && err.slice().shift() === 404) {
|
|
221
242
|
return cbk();
|
|
222
243
|
}
|
|
@@ -247,24 +268,100 @@ module.exports = (args, cbk) => {
|
|
|
247
268
|
cbk);
|
|
248
269
|
}],
|
|
249
270
|
|
|
271
|
+
// Intercept virtual invoice forwards
|
|
272
|
+
interceptVirtualInvoice: ['addInvoice', ({addInvoice}, cbk) => {
|
|
273
|
+
// Exit early when not intercepting the virtual forward
|
|
274
|
+
if (!args.is_virtual) {
|
|
275
|
+
return cbk();
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
args.logger.info({listening_for_virtual_channel_payment: true});
|
|
279
|
+
|
|
280
|
+
const sub = subscribeToForwardRequests({lnd: args.lnd});
|
|
281
|
+
|
|
282
|
+
// Stop listening for the HTLC when the invoice expires
|
|
283
|
+
const timeout = setTimeout(() => {
|
|
284
|
+
sub.removeAllListeners();
|
|
285
|
+
|
|
286
|
+
return cbk([408, 'TimedOutWaitingForPayment']);
|
|
287
|
+
},
|
|
288
|
+
new Date(parseRequest(addInvoice.request).expires_at) - new Date());
|
|
289
|
+
|
|
290
|
+
const finished = (err, res) => {
|
|
291
|
+
clearTimeout(timeout);
|
|
292
|
+
|
|
293
|
+
sub.removeAllListeners();
|
|
294
|
+
|
|
295
|
+
return cbk(err, res);
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
// Listen for a payment to the virtual channel invoice
|
|
299
|
+
sub.on('forward_request', async forward => {
|
|
300
|
+
// Exit early and accept requests that are not for this invoice
|
|
301
|
+
if (forward.hash !== addInvoice.id) {
|
|
302
|
+
return forward.accept({});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
args.logger.info({accepting_payment: true});
|
|
306
|
+
|
|
307
|
+
forward.settle({secret: addInvoice.secret});
|
|
308
|
+
|
|
309
|
+
// Listen for an error on the requests subscription
|
|
310
|
+
sub.on('error', err => {
|
|
311
|
+
args.logger.error({err});
|
|
312
|
+
|
|
313
|
+
return finished(err);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
// Wait until the payment is no longer pending
|
|
317
|
+
await asyncRetry({interval, times}, async () => {
|
|
318
|
+
const {channels} = await getChannels({lnd: args.lnd});
|
|
319
|
+
|
|
320
|
+
const channel = channels.find(n => n.id === forward.in_channel);
|
|
321
|
+
|
|
322
|
+
if (!channel) {
|
|
323
|
+
throw new Error('FailedToFindForwardChannel');
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const pending = channel.pending_payments.find(({payment}) => {
|
|
327
|
+
return payment === forward.in_channel;
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
if (!!pending) {
|
|
331
|
+
throw new Error('PaymentIsStillPending');
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return;
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
const got = BigInt(forward.mtokens) + BigInt(forward.fee_mtokens);
|
|
338
|
+
|
|
339
|
+
args.logger.info({received: mtokensAsBigUnit(got)});
|
|
340
|
+
|
|
341
|
+
return finished();
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
return;
|
|
345
|
+
}],
|
|
346
|
+
|
|
250
347
|
// Create the final signed public payment request
|
|
251
348
|
publicRequest: [
|
|
252
349
|
'addInvoice',
|
|
253
|
-
'
|
|
350
|
+
'getId',
|
|
254
351
|
'getNetwork',
|
|
255
352
|
'getPolicies',
|
|
256
353
|
'parseAmount',
|
|
257
354
|
({
|
|
258
355
|
addInvoice,
|
|
259
|
-
|
|
356
|
+
getId,
|
|
260
357
|
getNetwork,
|
|
261
358
|
getPolicies,
|
|
262
359
|
parseAmount,
|
|
263
360
|
},
|
|
264
361
|
cbk) =>
|
|
265
362
|
{
|
|
266
|
-
// Exit early if not
|
|
267
|
-
if (!args.is_selecting_hops) {
|
|
363
|
+
// Exit early if not using custom hop hints
|
|
364
|
+
if (!args.is_selecting_hops && !args.is_virtual) {
|
|
268
365
|
return cbk(null, {
|
|
269
366
|
request: addInvoice.request,
|
|
270
367
|
tokens: addInvoice.tokens,
|
|
@@ -275,17 +372,35 @@ module.exports = (args, cbk) => {
|
|
|
275
372
|
channels: getPolicies,
|
|
276
373
|
cltv_delta: parseRequest(addInvoice.request).cltv_delta,
|
|
277
374
|
description: args.description || defaultInvoiceDescription,
|
|
278
|
-
destination:
|
|
375
|
+
destination: getId.public_key,
|
|
279
376
|
features: parseRequest(addInvoice.request).features,
|
|
280
377
|
id: addInvoice.id,
|
|
378
|
+
is_virtual: args.is_virtual,
|
|
281
379
|
lnd: args.lnd,
|
|
282
380
|
network: getNetwork.bitcoinjs,
|
|
283
381
|
payment: addInvoice.payment,
|
|
284
382
|
tokens: parseAmount.tokens,
|
|
383
|
+
virtual_fee_rate: args.virtual_fee_rate,
|
|
285
384
|
},
|
|
286
385
|
cbk);
|
|
287
386
|
}],
|
|
387
|
+
|
|
388
|
+
// Log the virtual channel payment request
|
|
389
|
+
logRequest: ['publicRequest', ({publicRequest}, cbk) => {
|
|
390
|
+
// Exit early when not using a virtual channel
|
|
391
|
+
if (!args.is_virtual) {
|
|
392
|
+
return cbk(null, publicRequest);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
args.logger.info({
|
|
396
|
+
request: publicRequest.request,
|
|
397
|
+
tokens: parseRequest(publicRequest.request).tokens,
|
|
398
|
+
virtual_fee_rate: args.virtual_fee_rate || undefined,
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
return cbk(null, {is_settled: true});
|
|
402
|
+
}],
|
|
288
403
|
},
|
|
289
|
-
returnResult({reject, resolve, of: '
|
|
404
|
+
returnResult({reject, resolve, of: 'logRequest'}, cbk));
|
|
290
405
|
});
|
|
291
406
|
};
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
+
const {randomBytes} = require('crypto');
|
|
2
|
+
|
|
1
3
|
const asyncAuto = require('async/auto');
|
|
2
4
|
const {createSignedRequest} = require('ln-service');
|
|
3
5
|
const {createUnsignedRequest} = require('ln-service');
|
|
4
6
|
const {decode} = require('bip66');
|
|
5
7
|
const {returnResult} = require('asyncjs-util');
|
|
8
|
+
const secp256k1 = require('secp256k1');
|
|
6
9
|
const {signBytes} = require('ln-service');
|
|
10
|
+
const tinysecp256k1 = require('tiny-secp256k1');
|
|
7
11
|
|
|
8
12
|
const bufferAsHex = buffer => buffer.toString('hex');
|
|
9
13
|
const {concat} = Buffer;
|
|
@@ -14,7 +18,11 @@ const hexAsBuffer = hex => Buffer.from(hex, 'hex');
|
|
|
14
18
|
const {isArray} = Array;
|
|
15
19
|
const keyFamilyIdentity = 6;
|
|
16
20
|
const keyIndexIdentity = 0;
|
|
21
|
+
const makePrivateKey = () => randomBytes(32);
|
|
22
|
+
const minimalCltvDelta = 18;
|
|
17
23
|
const rValue = r => r.length === 33 ? r.slice(1) : r;
|
|
24
|
+
const unit8AsHex = n => Buffer.from(n).toString('hex');
|
|
25
|
+
const virtualChannelId = '805x805x805';
|
|
18
26
|
|
|
19
27
|
/** Create a signed BOLT 11 payment request
|
|
20
28
|
|
|
@@ -95,8 +103,38 @@ module.exports = (args, cbk) => {
|
|
|
95
103
|
return cbk();
|
|
96
104
|
},
|
|
97
105
|
|
|
106
|
+
// Create a key pair for a virtual channel invoice
|
|
107
|
+
getKeyPair: ['validate', ({}, cbk) => {
|
|
108
|
+
// Exit early when not using a virtual channel
|
|
109
|
+
if (!args.is_virtual) {
|
|
110
|
+
return cbk();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const privateKey = makePrivateKey();
|
|
114
|
+
|
|
115
|
+
const publicKey = unit8AsHex(secp256k1.publicKeyCreate(privateKey));
|
|
116
|
+
|
|
117
|
+
return cbk(null, {private_key: privateKey, public_key: publicKey});
|
|
118
|
+
}],
|
|
119
|
+
|
|
98
120
|
// Assemble the hop hints from the chosen hint channels
|
|
99
|
-
hints: ['
|
|
121
|
+
hints: ['getKeyPair', ({getKeyPair}, cbk) => {
|
|
122
|
+
// Exit early when using a virtual channel
|
|
123
|
+
if (!!args.is_virtual) {
|
|
124
|
+
return cbk(null, [[
|
|
125
|
+
{
|
|
126
|
+
public_key: args.destination,
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
base_fee_mtokens: Number().toString(),
|
|
130
|
+
channel: virtualChannelId,
|
|
131
|
+
cltv_delta: minimalCltvDelta,
|
|
132
|
+
fee_rate: args.virtual_fee_rate || Number(),
|
|
133
|
+
public_key: getKeyPair.public_key,
|
|
134
|
+
},
|
|
135
|
+
]]);
|
|
136
|
+
}
|
|
137
|
+
|
|
100
138
|
const routes = args.channels.map(({id, policies}) => {
|
|
101
139
|
const peerPolicy = policies.find(policy => {
|
|
102
140
|
return policy.public_key !== args.destination;
|
|
@@ -121,11 +159,13 @@ module.exports = (args, cbk) => {
|
|
|
121
159
|
|
|
122
160
|
// Create the unsigned payment request
|
|
123
161
|
unsigned: ['hints', ({hints}, cbk) => {
|
|
162
|
+
const [destination] = hints.slice().reverse();
|
|
163
|
+
|
|
124
164
|
try {
|
|
125
165
|
const unsigned = createUnsignedRequest({
|
|
126
166
|
cltv_delta: args.cltv_delta,
|
|
127
167
|
description: args.description,
|
|
128
|
-
destination:
|
|
168
|
+
destination: destination.public_key,
|
|
129
169
|
features: args.features,
|
|
130
170
|
id: args.id,
|
|
131
171
|
network: args.network,
|
|
@@ -141,28 +181,52 @@ module.exports = (args, cbk) => {
|
|
|
141
181
|
}],
|
|
142
182
|
|
|
143
183
|
// Sign the unsigned payment request
|
|
144
|
-
sign: ['unsigned', ({unsigned}, cbk) => {
|
|
184
|
+
sign: ['getKeyPair', 'unsigned', ({getKeyPair, unsigned}, cbk) => {
|
|
185
|
+
// Exit early when signing using the virtual key
|
|
186
|
+
if (!!args.is_virtual) {
|
|
187
|
+
const signature = tinysecp256k1.sign(
|
|
188
|
+
hexAsBuffer(unsigned.hash),
|
|
189
|
+
getKeyPair.private_key
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
return cbk(null, {
|
|
193
|
+
destination: getKeyPair.public_key,
|
|
194
|
+
signature: unit8AsHex(signature),
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Sign the modified payment request using the identity public key
|
|
145
199
|
return signBytes({
|
|
146
200
|
key_family: keyFamilyIdentity,
|
|
147
201
|
key_index: keyIndexIdentity,
|
|
148
202
|
lnd: args.lnd,
|
|
149
203
|
preimage: unsigned.preimage,
|
|
150
204
|
},
|
|
151
|
-
|
|
205
|
+
(err, res) => {
|
|
206
|
+
if (!!err) {
|
|
207
|
+
return cbk(err);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Convert the signature format
|
|
211
|
+
const {r, s} = decode(hexAsBuffer(res.signature));
|
|
212
|
+
|
|
213
|
+
return cbk(null, {
|
|
214
|
+
destination: args.destination,
|
|
215
|
+
signature: bufferAsHex(concat([rValue(r), s])),
|
|
216
|
+
});
|
|
217
|
+
});
|
|
152
218
|
}],
|
|
153
219
|
|
|
154
|
-
// Assemble the signed request
|
|
220
|
+
// Assemble the full signed request
|
|
155
221
|
request: ['sign', 'unsigned', ({sign, unsigned}, cbk) => {
|
|
156
222
|
try {
|
|
157
|
-
const {r, s} = decode(hexAsBuffer(sign.signature));
|
|
158
|
-
|
|
159
223
|
const {request} = createSignedRequest({
|
|
160
|
-
destination:
|
|
224
|
+
destination: sign.destination,
|
|
161
225
|
hrp: unsigned.hrp,
|
|
162
|
-
signature:
|
|
226
|
+
signature: sign.signature,
|
|
163
227
|
tags: unsigned.tags,
|
|
164
228
|
});
|
|
165
|
-
|
|
229
|
+
|
|
166
230
|
return cbk(null, {request, tokens: args.tokens});
|
|
167
231
|
} catch (err) {
|
|
168
232
|
return cbk([503, 'UnexpectedErrorSigningRequest', {err}]);
|
package/package.json
CHANGED
|
@@ -83,5 +83,5 @@
|
|
|
83
83
|
"postpublish": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t alexbosworth/balanceofsatoshis --push .",
|
|
84
84
|
"test": "tap --branches=1 --functions=1 --lines=1 --statements=1 -t 60 test/arrays/*.js test/balances/*.js test/chain/*.js test/display/*.js test/encryption/*.js test/lnd/*.js test/network/*.js test/nodes/*.js test/peers/*.js test/responses/*.js test/routing/*.js test/services/*.js test/swaps/*.js test/tags/*.js test/telegram/*.js test/wallets/*.js"
|
|
85
85
|
},
|
|
86
|
-
"version": "13.
|
|
86
|
+
"version": "13.8.0"
|
|
87
87
|
}
|