balanceofsatoshis 12.5.1 → 12.6.2

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
+ ## 12.6.2
4
+
5
+ - `telegram`: Avoid bot crash when receiving an AMP push payment
6
+
7
+ ## 12.6.1
8
+
9
+ - `lnurl`: Add support for `channel` to request an inbound channel
10
+
3
11
  ## 12.5.1
4
12
 
5
13
  - `open-balanced-channel`: Correct potential future regression in signing
package/bos CHANGED
@@ -1024,6 +1024,7 @@ prog
1024
1024
  .argument('<function>', 'lnurl function', keys(lnurlFunctions))
1025
1025
  .help(`Functions: ${keys(lnurlFunctions).join(', ')}`)
1026
1026
  .help('lnurl auth will request authorization')
1027
+ .help('lnurl channel will request an incoming payment channel')
1027
1028
  .help('lnurl pay will request a payment request from a service')
1028
1029
  .help('lnurl withdraw will create an invoice and send it to a service')
1029
1030
  .option('--avoid <avoid>', 'Avoid forwarding via node/chan/tag', REPEATABLE)
@@ -9,6 +9,7 @@
9
9
  },
10
10
  "lnurlFunctions": {
11
11
  "auth": "auth",
12
+ "channel": "channel",
12
13
  "pay": "pay",
13
14
  "withdraw": "withdraw"
14
15
  },
@@ -0,0 +1,295 @@
1
+ const {isIP} = require('net');
2
+
3
+ const asyncAuto = require('async/auto');
4
+ const {bech32} = require('bech32');
5
+ const {addPeer} = require('ln-service');
6
+ const {getIdentity} = require('ln-service');
7
+ const {getNodeAlias} = require('ln-sync');
8
+ const {getPeers} = require('ln-service');
9
+ const {returnResult} = require('asyncjs-util');
10
+
11
+ const asLnurl = n => n.substring(n.startsWith('lightning:') ? 10 : 0);
12
+ const bech32CharLimit = 2000;
13
+ const {decode} = bech32;
14
+ const errorStatus = 'ERROR';
15
+ const isPublicKey = n => !!n && /^0[2-3][0-9A-F]{64}$/i.test(n);
16
+ const okStatus = 'OK';
17
+ const parseUri = n => n.split('@');
18
+ const prefix = 'lnurl';
19
+ const sslProtocol = 'https:';
20
+ const tag = 'channelRequest';
21
+ const typeDefault = '0';
22
+ const types = [{name: 'Public', value: '0'}, {name: 'Private', value: '1'}];
23
+ const wordsAsUtf8 = n => Buffer.from(bech32.fromWords(n)).toString('utf8');
24
+
25
+ /** Request inbound channel from lnurl
26
+
27
+ {
28
+ ask: <Ask Function>
29
+ lnd: <Authenticated LND API Object>
30
+ lnurl: <Lnurl String>
31
+ logger: <Winston Logger Object>
32
+ request: <Request Function>
33
+ }
34
+
35
+ @returns via cbk or Promise
36
+ */
37
+ module.exports = (args, cbk) => {
38
+ return new Promise((resolve, reject) => {
39
+ return asyncAuto({
40
+ // Check arguments
41
+ validate: cbk => {
42
+ if (!args.ask) {
43
+ return cbk([400, 'ExpectedAskFunctionToRequestChannelFromLnurl']);
44
+ }
45
+
46
+ if (!args.lnurl) {
47
+ return cbk([400, 'ExpectedUrlToRequestChannelFromLnurl']);
48
+ }
49
+
50
+ try {
51
+ decode(asLnurl(args.lnurl), bech32CharLimit);
52
+ } catch (err) {
53
+ return cbk([400, 'FailedToDecodeLnurlToRequestChannel', {err}]);
54
+ }
55
+
56
+ if (decode(asLnurl(args.lnurl), bech32CharLimit).prefix !== prefix) {
57
+ return cbk([400, 'ExpectedLnUrlPrefixToRequestChannel']);
58
+ }
59
+
60
+ if (!args.lnd) {
61
+ return cbk([400, 'ExpectedLndToRequestChannelFromLnurl']);
62
+ }
63
+
64
+ if (!args.logger) {
65
+ return cbk([400, 'ExpectedLoggerToRequestChannelFromLnurl']);
66
+ }
67
+
68
+ if (!args.request) {
69
+ return cbk([400, 'ExpectedRequestFunctionToGetLnurlRequestChannel']);
70
+ }
71
+
72
+ return cbk();
73
+ },
74
+
75
+ // Get node identity public key
76
+ getIdentity: ['validate', ({}, cbk) => {
77
+ return getIdentity({lnd: args.lnd}, cbk);
78
+ }],
79
+
80
+ // Get the list of connected peers to determine if connection is needed
81
+ getPeers: ['validate', ({}, cbk) => getPeers({lnd: args.lnd}, cbk)],
82
+
83
+ // Get accepted terms from the encoded url
84
+ getTerms: ['validate', ({}, cbk) => {
85
+ const {words} = decode(asLnurl(args.lnurl), bech32CharLimit);
86
+
87
+ const url = wordsAsUtf8(words);
88
+
89
+ return args.request({url, json: true}, (err, r, json) => {
90
+ if (!!err) {
91
+ return cbk([503, 'FailureGettingLnurlDataFromUrl', {err}]);
92
+ }
93
+
94
+ if (!json) {
95
+ return cbk([503, 'ExpectedJsonObjectReturnedInLnurlResponse']);
96
+ }
97
+
98
+ if (json.status === errorStatus) {
99
+ return cbk([503, 'UnexpectedServiceError', {err: json.reason}]);
100
+ }
101
+
102
+ if (!json.callback) {
103
+ return cbk([503, 'ExpectedCallbackInLnurlResponseJson']);
104
+ }
105
+
106
+ try {
107
+ new URL(json.callback);
108
+ } catch (err) {
109
+ return cbk([503, 'ExpectedValidLnurlResponseCallbackUrl', {err}]);
110
+ }
111
+
112
+ if ((new URL(json.callback)).protocol !== sslProtocol) {
113
+ return cbk([400, 'LnurlsThatSpecifyNonSslUrlsAreUnsupported']);
114
+ }
115
+
116
+ if (!json.k1) {
117
+ return cbk([503, 'ExpectedK1InLnurlChannelResponseJson']);
118
+ }
119
+
120
+ if (!json.tag) {
121
+ return cbk([503, 'ExpectedTagInLnurlChannelResponseJson']);
122
+ }
123
+
124
+ if (json.tag !== tag) {
125
+ return cbk([503, 'ExpectedTagToBeChannelRequestInLnurlResponse']);
126
+ }
127
+
128
+ if (!json.uri) {
129
+ return cbk([503, 'ExpectedUriInLnurlResponseJson']);
130
+ }
131
+
132
+ // uri: remote node address of form node_key@ip_address:port_number
133
+ const [id, socket] = parseUri(json.uri);
134
+
135
+ if (!isPublicKey(id)) {
136
+ return cbk([503, 'ExpectedValidPublicKeyIdInLnurlResponseJson']);
137
+ }
138
+
139
+ if (!socket) {
140
+ return cbk([503, 'ExpectedNetworkSocketAddressInLnurlResponse']);
141
+ }
142
+
143
+ return cbk(null, {id, socket, k1: json.k1, url: json.callback});
144
+ });
145
+ }],
146
+
147
+ // Get the node alias
148
+ getAlias: ['getTerms', ({getTerms}, cbk) => {
149
+ return getNodeAlias({id: getTerms.id, lnd: args.lnd}, cbk);
150
+ }],
151
+
152
+ // Connect to the peer returned in the lnurl response
153
+ connect: [
154
+ 'getAlias',
155
+ 'getPeers',
156
+ 'getTerms',
157
+ ({getAlias, getPeers, getTerms}, cbk) =>
158
+ {
159
+ // Exit early when the node is already connected
160
+ if (getPeers.peers.map(n => n.public_key).includes(getTerms.id)) {
161
+ return cbk();
162
+ }
163
+
164
+ args.logger.info({
165
+ connecting_to: {
166
+ alias: getAlias.alias || undefined,
167
+ public_key: getTerms.id,
168
+ socket: getTerms.socket,
169
+ },
170
+ });
171
+
172
+ return addPeer({
173
+ lnd: args.lnd,
174
+ public_key: getTerms.id,
175
+ socket: getTerms.socket,
176
+ },
177
+ cbk);
178
+ }],
179
+
180
+ // Select private or public mode for the channel
181
+ askPrivate: ['connect', 'getTerms', ({getTerms}, cbk) => {
182
+ return args.ask({
183
+ choices: types,
184
+ default: typeDefault,
185
+ message: 'Channel type?',
186
+ name: 'priv',
187
+ type: 'list',
188
+ },
189
+ ({priv}) => cbk(null, priv));
190
+ }],
191
+
192
+ // Confirm that an inbound channel should be requested
193
+ ok: ['askPrivate', 'getAlias', ({askPrivate, getAlias}, cbk) => {
194
+ const node = getAlias.alias || getAlias.id;
195
+ const type = !!askPrivate ? 'a private' : 'an';
196
+
197
+ return args.ask({
198
+ default: true,
199
+ message: `Request ${type} inbound channel from ${node}?`,
200
+ name: 'ok',
201
+ type: 'confirm',
202
+ },
203
+ ({ok}) => cbk(null, ok));
204
+ }],
205
+
206
+ // Send a signal to cancel the channel request
207
+ sendCancelation: [
208
+ 'getIdentity',
209
+ 'getTerms',
210
+ 'ok',
211
+ ({channel, getTerms, ok}, cbk) =>
212
+ {
213
+ // Exit early when user wants to proceed with the channel request
214
+ if (!!ok) {
215
+ return cbk();
216
+ }
217
+
218
+ return args.request({
219
+ json: true,
220
+ qs: {
221
+ cancel: Number(!ok),
222
+ k1: getTerms.k1,
223
+ remoteid: getIdentity.public_key,
224
+ },
225
+ url: getTerms.url,
226
+ },
227
+ (err, r, json) => {
228
+ if (!!err) {
229
+ return cbk([503, 'UnexpectedErrorCancelingChannelRequest', {err}]);
230
+ }
231
+
232
+ if (!json) {
233
+ return cbk([503, 'ExpectedJsonObjectInCancelChannelResponse']);
234
+ }
235
+
236
+ if (json.status === errorStatus) {
237
+ return cbk([503, 'ChannelCancelReturnedErr', {err: json.reason}]);
238
+ }
239
+
240
+ if (json.status !== okStatus) {
241
+ return cbk([503, 'ExpectedOkStatusInCancelChannelResponse']);
242
+ }
243
+
244
+ return cbk([400, 'CanceledRequestForInboundChannel']);
245
+ });
246
+ }],
247
+
248
+ // Make the request to confirm a request for an inbound channel
249
+ sendConfirmation: [
250
+ 'askPrivate',
251
+ 'getIdentity',
252
+ 'getTerms',
253
+ 'ok',
254
+ ({askPrivate, getIdentity, getTerms, ok}, cbk) =>
255
+ {
256
+ // Exit early when the user decides to cancel
257
+ if (!ok) {
258
+ return cbk();
259
+ }
260
+
261
+ return args.request({
262
+ json: true,
263
+ qs: {
264
+ k1: getTerms.k1,
265
+ private: askPrivate,
266
+ remoteid: getIdentity.public_key,
267
+ },
268
+ url: getTerms.url,
269
+ },
270
+ (err, r, json) => {
271
+ if (!!err) {
272
+ return cbk([503, 'UnexpectedErrorRequestingLnurlChannel', {err}]);
273
+ }
274
+
275
+ if (!json) {
276
+ return cbk([503, 'ExpectedJsonObjectReturnedInChannelResponse']);
277
+ }
278
+
279
+ if (json.status === errorStatus) {
280
+ return cbk([503, 'ChannelRequestReturnedErr', {err: json.reason}]);
281
+ }
282
+
283
+ if (json.status !== okStatus) {
284
+ return cbk([503, 'ExpectedOkStatusInChannelRequestResponse']);
285
+ }
286
+
287
+ args.logger.info({requested_channel_open: true});
288
+
289
+ return cbk();
290
+ });
291
+ }],
292
+ },
293
+ returnResult({reject, resolve}, cbk));
294
+ });
295
+ };
@@ -2,13 +2,16 @@ const asyncAuto = require('async/auto');
2
2
  const {returnResult} = require('asyncjs-util');
3
3
 
4
4
  const auth = require('./auth');
5
+ const channel = require('./channel');
5
6
  const pay = require('./pay');
6
7
  const withdraw = require('./withdraw');
7
8
 
8
9
  const functionAuth = 'auth';
10
+ const functionChannel = 'channel';
9
11
  const functionPay = 'pay';
10
12
  const functionWithdraw = 'withdraw';
11
13
  const {isArray} = Array;
14
+ const supportedFunctions = ['auth', 'channel', 'pay', 'withdraw'];
12
15
 
13
16
  /** Manage Lnurl functions
14
17
 
@@ -40,7 +43,7 @@ module.exports = (args, cbk) => {
40
43
  return cbk([400, 'ExpectedArrayOfAvoidsToManageLnurl']);
41
44
  }
42
45
 
43
- if (![functionAuth, functionPay, functionWithdraw].includes(args.function)) {
46
+ if (!supportedFunctions.includes(args.function)) {
44
47
  return cbk([400, 'ExpectedLnurlFunctionToManageLnurl']);
45
48
  }
46
49
 
@@ -84,6 +87,23 @@ module.exports = (args, cbk) => {
84
87
  cbk);
85
88
  }],
86
89
 
90
+ // Request inbound channel
91
+ channel: ['validate', ({}, cbk) => {
92
+ // Exit early if not lnurl channel
93
+ if (args.function !== functionChannel) {
94
+ return cbk();
95
+ }
96
+
97
+ return channel({
98
+ ask: args.ask,
99
+ lnd: args.lnd,
100
+ lnurl: args.lnurl,
101
+ logger: args.logger,
102
+ request: args.request,
103
+ },
104
+ cbk);
105
+ }],
106
+
87
107
  // Pay to lnurl
88
108
  pay: ['validate', ({}, cbk) => {
89
109
  // Exit early if not lnurl pay
package/package.json CHANGED
@@ -36,11 +36,11 @@
36
36
  "ini": "3.0.0",
37
37
  "inquirer": "8.2.2",
38
38
  "ln-accounting": "5.0.6",
39
- "ln-service": "53.14.1",
39
+ "ln-service": "53.15.0",
40
40
  "ln-sync": "3.12.0",
41
41
  "ln-telegram": "3.21.2",
42
42
  "moment": "2.29.3",
43
- "paid-services": "3.15.2",
43
+ "paid-services": "3.15.4",
44
44
  "probing": "2.0.5",
45
45
  "psbt": "2.0.1",
46
46
  "qrcode-terminal": "0.12.0",
@@ -84,5 +84,5 @@
84
84
  "postpublish": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t alexbosworth/balanceofsatoshis --push .",
85
85
  "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"
86
86
  },
87
- "version": "12.5.1"
87
+ "version": "12.6.2"
88
88
  }
@@ -58,6 +58,7 @@ const ask = (n, cbk) => inquirer.prompt(n).then(n => cbk(n));
58
58
  const fileAsDoc = file => new InputFile(file.source, file.filename);
59
59
  const fromName = node => `${node.alias} ${node.public_key.substring(0, 8)}`;
60
60
  const {isArray} = Array;
61
+ const isHash = n => /^[0-9A-F]{64}$/i.test(n);
61
62
  let isBotInit = false;
62
63
  const isNumber = n => !isNaN(n);
63
64
  const limit = 99999;
@@ -694,6 +695,11 @@ module.exports = (args, cbk) => {
694
695
  subscriptions.push(sub);
695
696
 
696
697
  sub.on('invoice_updated', invoice => {
698
+ // Exit early when an invoice has no associated hash
699
+ if (!isHash(invoice.id)) {
700
+ return;
701
+ }
702
+
697
703
  return postSettledInvoice({
698
704
  from: node.from,
699
705
  id: connectedId,