balanceofsatoshis 13.6.0 → 13.7.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.7.0
4
+
5
+ - `invoice`: Add command to create a new invoice
6
+
3
7
  ## 13.6.0
4
8
 
5
9
  - `tags`: Support tag icons in routing failure source descriptions
package/bos CHANGED
@@ -32,6 +32,7 @@ const lnurl = importLazy('./lnurl');
32
32
  const {lnurlFunctions} = commandConstants;
33
33
  const network = importLazy('./network');
34
34
  const nodes = importLazy('./nodes');
35
+ const offchain = importLazy('./offchain');
35
36
  const {peerSortOptions} = commandConstants;
36
37
  const peers = importLazy('./peers');
37
38
  const {priceProviders} = commandConstants;
@@ -1051,6 +1052,37 @@ prog
1051
1052
  });
1052
1053
  })
1053
1054
 
1055
+ // Create an invoice
1056
+ .command('invoice', 'Create an invoice and get a BOLT 11 payment request')
1057
+ .help('Amount can take m/k variables: 5*m for 5 million, 250*k = 0.0025')
1058
+ .help('Fiat conversion is supported in amount, N*USD or N*EUR')
1059
+ .help(`Fiat rate providers: ${priceProviders.join(', ')}`)
1060
+ .argument('[amount]', 'Amount for invoice', STRING, '0')
1061
+ .option('--for <description>', 'What is the invoice requesting payment for')
1062
+ .option('--include-hints', 'Include the default set of hop hint channels')
1063
+ .option('--node <node_name>', 'Use saved node to create invoice')
1064
+ .option('--rate-provider <rate_provider>', 'Rate provider', priceProviders)
1065
+ .option('--select-hints', 'Select hop hints to be added to the request')
1066
+ .action((args, options, logger) => {
1067
+ return new Promise(async (resolve, reject) => {
1068
+ try {
1069
+ return await offchain.createInvoice({
1070
+ amount: args.amount,
1071
+ ask: await commands.interrogate({}),
1072
+ description: options.for,
1073
+ is_hinting: options.includeHints || undefined,
1074
+ is_selecting_hops: options.selectHints || undefined,
1075
+ lnd: (await lnd.authenticatedLnd({logger, node: options.node})).lnd,
1076
+ rate_provider: options.rateProvider || undefined,
1077
+ request: commands.simpleRequest,
1078
+ },
1079
+ responses.returnObject({exit, logger, reject, resolve}));
1080
+ } catch (err) {
1081
+ return logger.error({err}) && reject();
1082
+ }
1083
+ });
1084
+ })
1085
+
1054
1086
  // Join a group channel open
1055
1087
  .command('join-group-channel', 'Join a balanced channels group')
1056
1088
  .visible(false)
@@ -0,0 +1,290 @@
1
+ const asyncAuto = require('async/auto');
2
+ const asyncMap = require('async/map');
3
+ const {createInvoice} = require('ln-service');
4
+ const {getChannels} = require('ln-service');
5
+ const {getChannel} = require('ln-service');
6
+ const {getIdentity} = require('ln-service');
7
+ const {getNetwork} = require('ln-sync');
8
+ const {getNodeAlias} = require('ln-sync');
9
+ const {getPrices} = require('@alexbosworth/fiat');
10
+ const {parseAmount} = require('ln-accounting');
11
+ const {parsePaymentRequest} = require('ln-service');
12
+ const {returnResult} = require('asyncjs-util');
13
+
14
+ const signPaymentRequest = require('./sign_payment_request');
15
+
16
+ const coins = ['BTC'];
17
+ const defaultFiatRateProvider = 'coinbase';
18
+ const defaultInvoiceDescription = '';
19
+ const fiats = ['EUR', 'USD'];
20
+ const hasFiat = n => /(eur|usd)/gim.test(n);
21
+ const {isArray} = Array;
22
+ const {isInteger} = Number;
23
+ const isNumber = n => !isNaN(n);
24
+ const networks = {btc: 'BTC', btctestnet: 'BTC', btcregtest: 'BTC'};
25
+ const parseRequest = request => parsePaymentRequest({request});
26
+ const rateAsTokens = rate => 1e10 / rate;
27
+ const tokensAsBigUnit = tokens => (tokens / 1e8).toFixed(8);
28
+ const uniq = arr => Array.from(new Set(arr));
29
+
30
+ /** Create an invoice for a requested amount
31
+
32
+ {
33
+ amount: <Invoice Amount String>
34
+ ask: <Inquirer Function>
35
+ [description]: <Invoice Description String>
36
+ [is_hinting]: <Include Private Channels Bool>
37
+ [is_selecting_hops]: <Is Selecting Hops Bool>
38
+ lnd: <Authenticated LND API Object>
39
+ [rate_provider]: <Fiat Rate Provider String>
40
+ request: <Request Function>
41
+ }
42
+
43
+ @returns via cbk or Promise
44
+ {
45
+ request: <BOLT 11 Payment Request String>
46
+ tokens: <Invoice Amount Number>
47
+ }
48
+ */
49
+ module.exports = (args, cbk) => {
50
+ return new Promise((resolve, reject) => {
51
+ return asyncAuto({
52
+ // Check arguments
53
+ validate: cbk => {
54
+ if (!args.amount) {
55
+ return cbk([400, 'ExpectedInvoiceAmountToCreateNewInvoice']);
56
+ }
57
+
58
+ if (isNumber(args.amount) && !isInteger(Number(args.amount))) {
59
+ return cbk([400, 'ExpectedIntegerAmountToInvoice']);
60
+ }
61
+
62
+ if (!args.ask) {
63
+ return cbk([400, 'ExpectedAskFunctionToCreateNewInvoice']);
64
+ }
65
+
66
+ if (!!args.is_hinting && !!args.is_selecting_hops) {
67
+ return cbk([400, 'CannotUseDefaultHintsAndAlsoSelectHints']);
68
+ }
69
+
70
+ if (!args.lnd) {
71
+ return cbk([400, 'ExpectedAuthenticatedLndToCreateNewInvoice']);
72
+ }
73
+
74
+ if (!args.request) {
75
+ return cbk([400, 'ExpectedRequestFunctionToCreateNewInvoice']);
76
+ }
77
+
78
+ return cbk();
79
+ },
80
+
81
+ // Get the current price of BTC in USD/EUR
82
+ getFiatPrice: ['validate', ({}, cbk) => {
83
+ // Exit early when no fiat is referenced
84
+ if (!hasFiat(args.amount)) {
85
+ return cbk();
86
+ }
87
+
88
+ return getPrices({
89
+ from: args.rate_provider || defaultFiatRateProvider,
90
+ request: args.request,
91
+ symbols: [].concat(fiats),
92
+ },
93
+ cbk);
94
+ }],
95
+
96
+ // Get channels to allow for selecting individual hop hints
97
+ getChannels: ['validate', ({}, cbk) => {
98
+ // Exit early when not selecting hop hints
99
+ if (!args.is_selecting_hops) {
100
+ return cbk();
101
+ }
102
+
103
+ return getChannels({
104
+ is_active: true,
105
+ is_private: true,
106
+ lnd: args.lnd,
107
+ },
108
+ cbk);
109
+ }],
110
+
111
+ // Get node aliases for channels for selecting hop hints
112
+ getAliases: ['getChannels', ({getChannels}, cbk) => {
113
+ // Exit early when not selecting hop hints
114
+ if (!args.is_selecting_hops) {
115
+ return cbk();
116
+ }
117
+
118
+ const ids = uniq(getChannels.channels.map(n => n.partner_public_key));
119
+
120
+ return asyncMap(ids, (id, cbk) => {
121
+ return getNodeAlias({id, lnd: args.lnd}, cbk);
122
+ },
123
+ cbk);
124
+ }],
125
+
126
+ // Get network name
127
+ getNetwork: ['validate', ({}, cbk) => getNetwork({lnd: args.lnd}, cbk)],
128
+
129
+ // Get wallet info
130
+ getIdentity: ['validate', ({}, cbk) => getIdentity({lnd: args.lnd}, cbk)],
131
+
132
+ // Fiat rates
133
+ rates: [
134
+ 'getFiatPrice',
135
+ 'getNetwork',
136
+ ({getFiatPrice, getNetwork}, cbk) =>
137
+ {
138
+ // Exit early when there is no fiat
139
+ if (!getFiatPrice) {
140
+ return cbk();
141
+ }
142
+
143
+ if (!networks[getNetwork.network]) {
144
+ return cbk([400, 'UnsupportedNetworkForFiatPriceConversion']);
145
+ }
146
+
147
+ const rates = fiats.map(fiat => {
148
+ const {rate} = getFiatPrice.tickers.find(n => n.ticker === fiat);
149
+
150
+ return {fiat, unit: rateAsTokens(rate)};
151
+ });
152
+
153
+ return cbk(null, rates);
154
+ }],
155
+
156
+ // Parse the amount
157
+ parseAmount: ['rates', ({rates}, cbk) => {
158
+ const eur = !!rates ? rates.find(n => n.fiat === 'EUR') : null;
159
+ const usd = !!rates ? rates.find(n => n.fiat === 'USD') : null;
160
+
161
+ // Variables to use in amount
162
+ const variables = {
163
+ eur: !!eur ? eur.unit : undefined,
164
+ usd: !!usd ? usd.unit : undefined,
165
+ };
166
+
167
+ try {
168
+ const {tokens} = parseAmount({variables, amount: args.amount});
169
+
170
+ return cbk(null, {tokens});
171
+ } catch (err) {
172
+ return cbk([400, 'FailedToParseAmount', {err}]);
173
+ }
174
+ }],
175
+
176
+ // Select hop hint channels
177
+ selectChannels: [
178
+ 'getAliases',
179
+ 'getChannels',
180
+ 'parseAmount',
181
+ ({getAliases, getChannels}, cbk) =>
182
+ {
183
+ // Exit early if not selecting channels
184
+ if (!args.is_selecting_hops) {
185
+ return cbk();
186
+ }
187
+
188
+ if (!getChannels.channels.length) {
189
+ return cbk([400, 'NoRelevantChannelsToSelectAsHints']);
190
+ }
191
+
192
+ return args.ask({
193
+ choices: getChannels.channels.map(channel => {
194
+ const node = getAliases.find(({id}) => {
195
+ return id === channel.partner_public_key
196
+ });
197
+
198
+ const value = channel.id;
199
+ const inbound = `in: ${tokensAsBigUnit(channel.remote_balance)}`;
200
+ const outbound = `out: ${tokensAsBigUnit(channel.local_balance)}`;
201
+
202
+ return {
203
+ value,
204
+ name: `${value} ${node.alias}: ${inbound} | ${outbound}.`,
205
+ };
206
+ }),
207
+ loop: false,
208
+ message: `Channels to include as hints in the invoice?`,
209
+ name: 'id',
210
+ type: 'checkbox',
211
+ validate: input => !!input.length,
212
+ },
213
+ ({id}) => cbk(null, id));
214
+ }],
215
+
216
+ // Get the policies of selected channels
217
+ getPolicies: ['selectChannels', ({selectChannels}, cbk) => {
218
+ return asyncMap(selectChannels, (channel, cbk) => {
219
+ return getChannel({id: channel, lnd: args.lnd}, (err, res) => {
220
+ if (isArray(err) && err.slice().shift() === 404) {
221
+ return cbk();
222
+ }
223
+
224
+ if (!!err) {
225
+ return cbk(err);
226
+ }
227
+
228
+ // Exit early when the channel policies are not defined
229
+ if (!!res.policies.find(n => n.cltv_delta === undefined)) {
230
+ return cbk();
231
+ }
232
+
233
+ return cbk(null, res);
234
+ });
235
+ },
236
+ cbk);
237
+ }],
238
+
239
+ // Create the invoice in the LND database
240
+ addInvoice: ['getPolicies', 'parseAmount', ({parseAmount}, cbk) => {
241
+ return createInvoice({
242
+ description: args.description || defaultInvoiceDescription,
243
+ is_including_private_channels: args.is_hinting || undefined,
244
+ lnd: args.lnd,
245
+ tokens: parseAmount.tokens,
246
+ },
247
+ cbk);
248
+ }],
249
+
250
+ // Create the final signed public payment request
251
+ publicRequest: [
252
+ 'addInvoice',
253
+ 'getIdentity',
254
+ 'getNetwork',
255
+ 'getPolicies',
256
+ 'parseAmount',
257
+ ({
258
+ addInvoice,
259
+ getIdentity,
260
+ getNetwork,
261
+ getPolicies,
262
+ parseAmount,
263
+ },
264
+ cbk) =>
265
+ {
266
+ // Exit early if not selecting custom hop hints
267
+ if (!args.is_selecting_hops) {
268
+ return cbk(null, {
269
+ request: addInvoice.request,
270
+ tokens: addInvoice.tokens,
271
+ });
272
+ }
273
+
274
+ return signPaymentRequest({
275
+ channels: getPolicies,
276
+ cltv_delta: parseRequest(addInvoice.request).cltv_delta,
277
+ description: args.description || defaultInvoiceDescription,
278
+ destination: getIdentity.public_key,
279
+ id: addInvoice.id,
280
+ lnd: args.lnd,
281
+ network: getNetwork.bitcoinjs,
282
+ payment: addInvoice.payment,
283
+ tokens: parseAmount.tokens,
284
+ },
285
+ cbk);
286
+ }],
287
+ },
288
+ returnResult({reject, resolve, of: 'publicRequest'}, cbk));
289
+ });
290
+ };
@@ -0,0 +1,3 @@
1
+ const createInvoice = require('./create_invoice');
2
+
3
+ module.exports = {createInvoice};
@@ -0,0 +1,166 @@
1
+ const asyncAuto = require('async/auto');
2
+ const {createSignedRequest} = require('ln-service');
3
+ const {createUnsignedRequest} = require('ln-service');
4
+ const {decode} = require('bip66');
5
+ const {returnResult} = require('asyncjs-util');
6
+ const {signBytes} = require('ln-service');
7
+
8
+ const bufferAsHex = buffer => buffer.toString('hex');
9
+ const {concat} = Buffer;
10
+ const defaultBaseFee = '1000';
11
+ const defaultCltvDelta = 144;
12
+ const defaultFeeRate = '1';
13
+ const hexAsBuffer = hex => Buffer.from(hex, 'hex');
14
+ const {isArray} = Array;
15
+ const keyFamilyIdentity = 6;
16
+ const keyIndexIdentity = 0;
17
+ const rValue = r => r.length === 33 ? r.slice(1) : r;
18
+
19
+ /** Create a signed BOLT 11 payment request
20
+
21
+ {
22
+ channels: [{
23
+ id: <Channel Id String>
24
+ policies: [{
25
+ [base_fee_mtokens]: <Base Routing Fee Millitokens String>
26
+ [cltv_delta]: <Routing CLTV Delta Number>
27
+ [fee_rate]: <Routing PPM Fee Rate Number>
28
+ public_key: <Node Identity Public Key Hex String>
29
+ }]
30
+ }]
31
+ cltv_delta: <Invoice Final CLTV Delta Number>
32
+ description: <Invoice Description String>
33
+ destination: <Destination Public Key Hex String>
34
+ id: <Payment Hash Hex String>
35
+ lnd: <Authenticated LND API Object>
36
+ network: <BitcoinJs Network Name String>
37
+ payment: <Payment Nonce Hex String>
38
+ tokens: <Invoiced Amount Tokens Number>
39
+ }
40
+
41
+ @returns via cbk or Promise
42
+ {
43
+ request: <BOLT 11 Payment Request String>
44
+ tokens: <Invoiced Tokens Number>
45
+ }
46
+ */
47
+ module.exports = (args, cbk) => {
48
+ return new Promise((resolve, reject) => {
49
+ return asyncAuto({
50
+ // Check arguments
51
+ validate: cbk => {
52
+ if (!isArray(args.channels)) {
53
+ return cbk([400, 'ExpectedArrayOfChannelsToSignPaymentRequest']);
54
+ }
55
+
56
+ if (!args.cltv_delta) {
57
+ return cbk([400, 'ExpectedFinalCltvDeltaToSignPaymentRequest']);
58
+ }
59
+
60
+ if (args.description === undefined) {
61
+ return cbk([400, 'ExpectedInvoiceDescriptionToSignPaymentRequest']);
62
+ }
63
+
64
+ if (!args.destination) {
65
+ return cbk([400, 'ExpectedDestinationNodeIdToSignPaymentRequest']);
66
+ }
67
+
68
+ if (!args.id) {
69
+ return cbk([400, 'ExpectedPaymentHashToSignPaymentRequest']);
70
+ }
71
+
72
+ if (!args.lnd) {
73
+ return cbk([400, 'ExpectedAuthenticatedLndToSignPaymentRequest']);
74
+ }
75
+
76
+ if (!args.network) {
77
+ return cbk([400, 'ExpectedNetworkNameToSignPaymentRequest']);
78
+ }
79
+
80
+ if (!args.payment) {
81
+ return cbk([400, 'ExpectedPaymentNonceToSignPaymentRequest']);
82
+ }
83
+
84
+ if (args.tokens === undefined) {
85
+ return cbk([400, 'ExpectedTokensToInvoiceToSignPaymentRequest']);
86
+ }
87
+
88
+ return cbk();
89
+ },
90
+
91
+ // Assemble the hop hints from the chosen hint channels
92
+ hints: ['validate', ({}, cbk) => {
93
+ const routes = args.channels.map(({id, policies}) => {
94
+ const peerPolicy = policies.find(policy => {
95
+ return policy.public_key !== args.destination;
96
+ });
97
+
98
+ return [
99
+ {
100
+ public_key: peerPolicy.public_key,
101
+ },
102
+ {
103
+ base_fee_mtokens: peerPolicy.base_fee_mtokens || defaultBaseFee,
104
+ channel: id,
105
+ cltv_delta: peerPolicy.cltv_delta || defaultCltvDelta,
106
+ fee_rate: peerPolicy.fee_rate || defaultFeeRate,
107
+ public_key: args.destination,
108
+ },
109
+ ];
110
+ });
111
+
112
+ return cbk(null, routes);
113
+ }],
114
+
115
+ // Create the unsigned payment request
116
+ unsigned: ['hints', ({hints}, cbk) => {
117
+ try {
118
+ const unsigned = createUnsignedRequest({
119
+ cltv_delta: args.cltv_delta,
120
+ description: args.description,
121
+ destination: args.destination,
122
+ id: args.id,
123
+ network: args.network,
124
+ payment: args.payment,
125
+ routes: !!hints.length ? hints : undefined,
126
+ tokens: args.tokens,
127
+ });
128
+
129
+ return cbk(null, unsigned);
130
+ } catch (err) {
131
+ return cbk([500, 'UnexpectedErrorCreatingUnsignedRequest', {err}]);
132
+ }
133
+ }],
134
+
135
+ // Sign the unsigned payment request
136
+ sign: ['unsigned', ({unsigned}, cbk) => {
137
+ return signBytes({
138
+ key_family: keyFamilyIdentity,
139
+ key_index: keyIndexIdentity,
140
+ lnd: args.lnd,
141
+ preimage: unsigned.preimage,
142
+ },
143
+ cbk);
144
+ }],
145
+
146
+ // Assemble the signed request
147
+ request: ['sign', 'unsigned', ({sign, unsigned}, cbk) => {
148
+ try {
149
+ const {r, s} = decode(hexAsBuffer(sign.signature));
150
+
151
+ const {request} = createSignedRequest({
152
+ destination: args.destination,
153
+ hrp: unsigned.hrp,
154
+ signature: bufferAsHex(concat([rValue(r), s])),
155
+ tags: unsigned.tags,
156
+ });
157
+
158
+ return cbk(null, {request, tokens: args.tokens});
159
+ } catch (err) {
160
+ return cbk([503, 'UnexpectedErrorSigningRequest', {err}]);
161
+ }
162
+ }],
163
+ },
164
+ returnResult({reject, resolve, of: 'request'}, cbk));
165
+ });
166
+ };
package/package.json CHANGED
@@ -36,7 +36,7 @@
36
36
  "ini": "3.0.1",
37
37
  "inquirer": "9.1.4",
38
38
  "ln-accounting": "6.1.1",
39
- "ln-service": "54.2.6",
39
+ "ln-service": "54.3.0",
40
40
  "ln-sync": "4.0.5",
41
41
  "ln-telegram": "4.2.0",
42
42
  "moment": "2.29.4",
@@ -53,8 +53,8 @@
53
53
  "description": "Lightning balance CLI",
54
54
  "devDependencies": {
55
55
  "@alexbosworth/tap": "15.0.11",
56
- "invoices": "2.2.0",
57
- "ln-docker-daemons": "3.1.4",
56
+ "invoices": "2.2.1",
57
+ "ln-docker-daemons": "3.1.5",
58
58
  "mock-lnd": "1.4.4",
59
59
  "tiny-secp256k1": "2.2.1"
60
60
  },
@@ -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.6.0"
86
+ "version": "13.7.0"
87
87
  }