balanceofsatoshis 13.7.2 → 13.9.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,13 @@
1
1
  # Versions
2
2
 
3
+ ## 13.9.0
4
+
5
+ - `create-channel-group`: Add `--allow` to order and allow only specific nodes
6
+
7
+ ## 13.8.0
8
+
9
+ - `invoice`: Add `--virtual` and `--virtual-fee-rate` to use virtual channel
10
+
3
11
  ## 13.7.2
4
12
 
5
13
  - `invoice`: Fix payment encoding to include invoice feature bits
package/bos CHANGED
@@ -524,9 +524,11 @@ prog
524
524
  })
525
525
 
526
526
  // Coordinate a new group channel
527
- .command('create-group-channel', 'Coordinate balanced channels group')
528
- .visible(false)
529
- .help('Other nodes can join the group using join-group-channel')
527
+ .command('create-channel-group', 'Coordinate balanced channels group')
528
+ .help('Other nodes can join the group using join-channel-group')
529
+ .help('When using --allow, all joining nodes must be identified')
530
+ .help('To specify order of group pairings, order using --allow ordering')
531
+ .option('--allow <public_key>', 'Only allow these nodes to join', REPEATABLE)
530
532
  .option('--capacity <channel_capacity>', 'Channel capacity', INT, 5e6)
531
533
  .option('--fee-rate <per_vbyte>', 'Chain fee rate for open', INT)
532
534
  .option('--node <node_name>', 'Use saved node to create channels group')
@@ -543,6 +545,7 @@ prog
543
545
  logger,
544
546
  capacity: options.capacity,
545
547
  count: options.size,
548
+ members: flatten([options.allow].filter(n => !!n)),
546
549
  rate: options.feeRate || floor(defaultRate.tokens_per_vbyte),
547
550
  },
548
551
  responses.returnObject({exit, logger, reject, resolve}));
@@ -1057,24 +1060,31 @@ prog
1057
1060
  .help('Amount can take m/k variables: 5*m for 5 million, 250*k = 0.0025')
1058
1061
  .help('Fiat conversion is supported in amount, N*USD or N*EUR')
1059
1062
  .help(`Fiat rate providers: ${priceProviders.join(', ')}`)
1063
+ .help('--virtual invoices cannot be used with payers who probe before pay')
1064
+ .help('Only one --virtual invoice can be active at a time')
1060
1065
  .argument('[amount]', 'Amount for invoice', STRING, '0')
1061
1066
  .option('--for <description>', 'What is the invoice requesting payment for')
1062
1067
  .option('--include-hints', 'Include the default set of hop hint channels')
1063
1068
  .option('--node <node_name>', 'Use saved node to create invoice')
1064
1069
  .option('--rate-provider <rate_provider>', 'Rate provider', priceProviders)
1065
1070
  .option('--select-hints', 'Select hop hints to be added to the request')
1071
+ .option('--virtual', 'Request payment over a virtual channel')
1072
+ .option('--virtual-fee-rate <pm>', 'Fee rate to use on virtual channel', INT)
1066
1073
  .action((args, options, logger) => {
1067
1074
  return new Promise(async (resolve, reject) => {
1068
1075
  try {
1069
1076
  return await offchain.createInvoice({
1077
+ logger,
1070
1078
  amount: args.amount,
1071
1079
  ask: await commands.interrogate({}),
1072
1080
  description: options.for,
1073
1081
  is_hinting: options.includeHints || undefined,
1074
1082
  is_selecting_hops: options.selectHints || undefined,
1083
+ is_virtual: options.virtual || undefined,
1075
1084
  lnd: (await lnd.authenticatedLnd({logger, node: options.node})).lnd,
1076
1085
  rate_provider: options.rateProvider || undefined,
1077
1086
  request: commands.simpleRequest,
1087
+ virtual_fee_rate: options.virtualFeeRate,
1078
1088
  },
1079
1089
  responses.returnObject({exit, logger, reject, resolve}));
1080
1090
  } catch (err) {
@@ -1084,9 +1094,8 @@ prog
1084
1094
  })
1085
1095
 
1086
1096
  // Join a group channel open
1087
- .command('join-group-channel', 'Join a balanced channels group')
1088
- .visible(false)
1089
- .help('Another node should have run create-group-channel to create group')
1097
+ .command('join-channel-group', 'Join a balanced channels group')
1098
+ .help('Another node should have run create-channel-group to create group')
1090
1099
  .argument('<code>', 'Invite code to join group', STRING)
1091
1100
  .option('--max-fee-rate <per_vbyte>', 'Maximum fee/vbyte for open', INT)
1092
1101
  .option('--node <node_name>', 'Use saved node to join group')
@@ -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
@@ -40,7 +40,7 @@
40
40
  "ln-sync": "4.0.5",
41
41
  "ln-telegram": "4.2.0",
42
42
  "moment": "2.29.4",
43
- "paid-services": "4.0.5",
43
+ "paid-services": "4.1.0",
44
44
  "probing": "3.0.0",
45
45
  "psbt": "2.7.1",
46
46
  "qrcode-terminal": "0.12.0",
@@ -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.9.0"
87
87
  }