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 CHANGED
@@ -1,5 +1,9 @@
1
1
  # Versions
2
2
 
3
+ ## 13.8.0
4
+
5
+ - `invoice`: Add `--virtual` and `--virtual-fee-rate` to use virtual channel
6
+
3
7
  ## 13.7.2
4
8
 
5
9
  - `invoice`: Fix payment encoding to include invoice feature bits
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
- request: <BOLT 11 Payment Request String>
46
- tokens: <Invoice Amount Number>
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
- getIdentity: ['validate', ({}, cbk) => getIdentity({lnd: args.lnd}, cbk)],
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
- 'getIdentity',
350
+ 'getId',
254
351
  'getNetwork',
255
352
  'getPolicies',
256
353
  'parseAmount',
257
354
  ({
258
355
  addInvoice,
259
- getIdentity,
356
+ getId,
260
357
  getNetwork,
261
358
  getPolicies,
262
359
  parseAmount,
263
360
  },
264
361
  cbk) =>
265
362
  {
266
- // Exit early if not selecting custom hop hints
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: getIdentity.public_key,
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: 'publicRequest'}, cbk));
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: ['validate', ({}, cbk) => {
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: args.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
- cbk);
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: args.destination,
224
+ destination: sign.destination,
161
225
  hrp: unsigned.hrp,
162
- signature: bufferAsHex(concat([rValue(r), s])),
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.7.2"
86
+ "version": "13.8.0"
87
87
  }