balanceofsatoshis 11.6.2 → 11.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
+ ## Version 11.7.0
4
+
5
+ - `fund`: Add `MAX` variable to allow spending down specified UTXOs
6
+
3
7
  ## Version 11.6.2
4
8
 
5
9
  - `clean-failed-payments`: Add method to clean out failed past payments
package/bos CHANGED
@@ -636,6 +636,7 @@ prog
636
636
  .command('fund', 'Make a signed transaction spending on-chain funds')
637
637
  .help('Use LND UTXOs to craft a signed raw transaction sending to addresses')
638
638
  .help('Specify <address> <amount> <address> <amount> for addresses, amounts')
639
+ .help('Amounts support formulas, use MAX to reference selected utxos total')
639
640
  .argument('<address_amount...>', 'Address and amount to send')
640
641
  .option('--dryrun', 'Avoid locking up UTXOs')
641
642
  .option('--fee-rate <fee>', 'Per vbyte fee rate for on-chain tx fee', INT)
@@ -3,9 +3,11 @@ const asyncEach = require('async/each');
3
3
  const {formatTokens} = require('ln-sync');
4
4
  const {fundPsbt} = require('ln-service');
5
5
  const {getChainFeeRate} = require('ln-service');
6
+ const {getMaxFundAmount} = require('ln-sync');
6
7
  const {getUtxos} = require('ln-service');
7
8
  const {returnResult} = require('asyncjs-util');
8
9
  const {signPsbt} = require('ln-service');
10
+ const {Transaction} = require('bitcoinjs-lib');
9
11
  const {unlockUtxo} = require('ln-service');
10
12
 
11
13
  const {parseAmount} = require('./../display');
@@ -15,11 +17,15 @@ const asOutpoint = utxo => `${utxo.transaction_id}:${utxo.transaction_vout}`;
15
17
  const asInput = n => ({transaction_id: n.id, transaction_vout: n.vout});
16
18
  const asUtxo = n => ({id: n.slice(0, 64), vout: Number(n.slice(65))});
17
19
  const dustValue = 293;
20
+ const formattedFeeRate = n => n.toFixed(2);
21
+ const {fromHex} = Transaction;
22
+ const hasMaxAmount = amounts => !!amounts.find(n => !!n && !!/max/gim.test(n));
18
23
  const {isArray} = Array;
19
24
  const isOutpoint = n => !!n && /^[0-9A-F]{64}:[0-9]{1,6}$/i.test(n);
20
25
  const isPublicKey = n => !!n && /^0[2-3][0-9A-F]{64}$/i.test(n);
21
26
  const minConfs = 1;
22
27
  const sumOf = arr => arr.reduce((sum, n) => sum + n, Number());
28
+ const txHashAsTxId = hash => hash.reverse().toString('hex');
23
29
 
24
30
  /** Fund and sign a transaction
25
31
 
@@ -92,8 +98,13 @@ module.exports = (args, cbk) => {
92
98
  // Get the current fee rate
93
99
  getFee: ['validate', ({}, cbk) => getChainFeeRate({lnd: args.lnd}, cbk)],
94
100
 
95
- // Derive exact outputs
101
+ // Derive a list of outputs to guide input selection
96
102
  outputs: ['validate', ({}, cbk) => {
103
+ // Exit early when the amount is open ended and thus depends on inputs
104
+ if (hasMaxAmount(args.amounts)) {
105
+ return cbk();
106
+ }
107
+
97
108
  try {
98
109
  const outputs = args.addresses.map((address, i) => {
99
110
  const {tokens} = parseAmount({amount: args.amounts[i]});
@@ -107,19 +118,12 @@ module.exports = (args, cbk) => {
107
118
  }
108
119
  }],
109
120
 
110
- // Get UTXOs
111
- getUtxos: ['validate', ({}, cbk) => {
112
- // Exit early when not selecting UTXOs
113
- if (!args.is_selecting_utxos) {
114
- return cbk();
115
- }
116
-
117
- return getUtxos({lnd: args.lnd, min_confirmations: minConfs}, cbk);
118
- }],
121
+ // Get UTXOs to use for input selection and final fee rate calculation
122
+ getUtxos: ['validate', ({}, cbk) => getUtxos({lnd: args.lnd}, cbk)],
119
123
 
120
124
  // Select inputs to spend
121
125
  utxos: ['getUtxos', 'outputs', ({getUtxos, outputs}, cbk) => {
122
- // Exit early when UTXOs are specified already
126
+ // Exit early when UTXOs are all specified already
123
127
  if (!!args.utxos.length) {
124
128
  return cbk(null, args.utxos);
125
129
  }
@@ -129,15 +133,16 @@ module.exports = (args, cbk) => {
129
133
  return cbk(null, []);
130
134
  }
131
135
 
136
+ // Only selecting confirmed utxos is supported
137
+ const utxos = getUtxos.utxos.filter(n => !!n.confirmation_count);
138
+
132
139
  // Make sure there are some UTXOs to select
133
- if (!getUtxos.utxos.length) {
140
+ if (!utxos.length) {
134
141
  return cbk([400, 'WalletHasZeroConfirmedUtxos']);
135
142
  }
136
143
 
137
- const amounts = outputs.map(n => n.tokens);
138
-
139
144
  return args.ask({
140
- choices: getUtxos.utxos.map(utxo => ({
145
+ choices: utxos.map(utxo => ({
141
146
  name: `${asBigUnit(utxo.tokens)} ${asOutpoint(utxo)}`,
142
147
  value: asOutpoint(utxo),
143
148
  })),
@@ -145,14 +150,22 @@ module.exports = (args, cbk) => {
145
150
  name: 'inputs',
146
151
  type: 'checkbox',
147
152
  validate: input => {
153
+ // A selection is required
148
154
  if (!input.length) {
149
155
  return false;
150
156
  }
151
157
 
152
158
  const tokens = sumOf(input.map(utxo => {
153
- return getUtxos.utxos.find(n => asOutpoint(n) === utxo).tokens;
159
+ return utxos.find(n => asOutpoint(n) === utxo).tokens;
154
160
  }));
155
161
 
162
+ // Exit early when the amount is open ended
163
+ if (hasMaxAmount(args.amounts)) {
164
+ return true;
165
+ }
166
+
167
+ const amounts = outputs.map(n => n.tokens);
168
+
156
169
  const missingTok = asBigUnit(sumOf(amounts) - tokens);
157
170
 
158
171
  if (tokens < sumOf(amounts)) {
@@ -165,33 +178,113 @@ module.exports = (args, cbk) => {
165
178
  ({inputs}) => cbk(null, inputs));
166
179
  }],
167
180
 
181
+ // Calculate the maximum possible amount to fund for selected inputs
182
+ getMax: [
183
+ 'getFee',
184
+ 'getUtxos',
185
+ 'outputs',
186
+ 'utxos',
187
+ ({getFee, getUtxos, outputs, utxos}, cbk) =>
188
+ {
189
+ // Exit early when the amount is not open ended
190
+ if (!hasMaxAmount(args.amounts)) {
191
+ return cbk(null, {});
192
+ }
193
+
194
+ // Because of anchor channel requirements, don't allow open ended max
195
+ if (!utxos.length) {
196
+ return cbk([400, 'MaxAmountOnlySupportedWhenUtxosSpecified']);
197
+ }
198
+
199
+ const feeRate = args.fee_tokens_per_vbyte || getFee.tokens_per_vbyte;
200
+
201
+ // Find the local UTXOs in order to get the input values
202
+ const spend = utxos.map(outpoint => {
203
+ return getUtxos.utxos.find(n => asOutpoint(n) === outpoint);
204
+ });
205
+
206
+ // Make sure that all inputs are known
207
+ if (spend.filter(n => !n).length) {
208
+ return cbk([400, 'UnknownInputSelected', {known_utxos: spend}]);
209
+ }
210
+
211
+ return getMaxFundAmount({
212
+ addresses: args.addresses,
213
+ fee_tokens_per_vbyte: feeRate,
214
+ inputs: spend.map(utxo => ({
215
+ tokens: utxo.tokens,
216
+ transaction_id: utxo.transaction_id,
217
+ transaction_vout: utxo.transaction_vout,
218
+ })),
219
+ lnd: args.lnd,
220
+ },
221
+ cbk);
222
+ }],
223
+
224
+ // Parse amounts and put together the final set of outputs
225
+ finalOutputs: ['getMax', ({getMax}, cbk) => {
226
+ try {
227
+ const outputs = args.addresses.map((address, i) => {
228
+ const amount = args.amounts[i];
229
+ const variables = {max: getMax.max_tokens};
230
+
231
+ return {address, tokens: parseAmount({amount, variables}).tokens};
232
+ });
233
+
234
+ return cbk(null, outputs);
235
+ } catch (err) {
236
+ return cbk([400, err.message]);
237
+ }
238
+ }],
239
+
168
240
  // Create a funded PSBT
169
- fund: ['getFee', 'outputs', 'utxos', ({getFee, outputs, utxos}, cbk) => {
241
+ fund: [
242
+ 'finalOutputs',
243
+ 'getFee',
244
+ 'utxos',
245
+ ({finalOutputs, getFee, utxos}, cbk) =>
246
+ {
170
247
  const inputs = utxos.map(asUtxo).map(asInput);
171
- const fee = args.fee_tokens_per_vbyte || getFee.tokens_per_vbyte;
248
+ const feeRate = args.fee_tokens_per_vbyte || getFee.tokens_per_vbyte;
172
249
 
173
- if (!!outputs.filter(n => n.tokens < dustValue).length) {
250
+ if (!!finalOutputs.filter(n => n.tokens < dustValue).length) {
174
251
  return cbk([400, 'ExpectedNonDustAmountValueForFundingAmount']);
175
252
  }
176
253
 
177
254
  args.logger.info({
178
- fee_rate: fee,
179
- send_to: outputs.map(output => ({
180
- [output.address]: formatTokens({tokens: output.tokens}).display,
255
+ send_to: finalOutputs.map(({address, tokens}) => ({
256
+ [address]: formatTokens({tokens}).display,
181
257
  })),
258
+ requested_fee_rate: feeRate,
182
259
  });
183
260
 
184
261
  return fundPsbt({
185
- outputs,
262
+ fee_tokens_per_vbyte: feeRate,
186
263
  inputs: !!inputs.length ? inputs : undefined,
187
264
  lnd: args.lnd,
188
- fee_tokens_per_vbyte: fee,
265
+ outputs: finalOutputs,
189
266
  },
190
267
  cbk);
191
268
  }],
192
269
 
193
- // Unlock the locked UTXO in a dry run scenario
194
- unlock: ['fund', ({fund}, cbk) => {
270
+ // Sign the funded PSBT
271
+ sign: ['fund', ({fund}, cbk) => {
272
+ const [change] = fund.outputs.filter(n => !!n.is_change);
273
+ const total = sumOf(fund.outputs.map(n => n.tokens));
274
+
275
+ const tokens = !!change ? change.tokens : undefined;
276
+
277
+ args.logger.info({
278
+ change: !!tokens ? formatTokens({tokens}).display : undefined,
279
+ sum_of_outputs: formatTokens({tokens: total}).display,
280
+ spending_utxos: fund.inputs.map(asOutpoint),
281
+ });
282
+
283
+ return signPsbt({lnd: args.lnd, psbt: fund.psbt}, cbk);
284
+ }],
285
+
286
+ // Unlock the locked UTXOs in a dry run scenario
287
+ unlock: ['fund', 'sign', ({fund}, cbk) => {
195
288
  // Exit early and keep UTXOs locked when not a dry run
196
289
  if (!args.is_dry_run) {
197
290
  return cbk();
@@ -209,25 +302,35 @@ module.exports = (args, cbk) => {
209
302
  cbk);
210
303
  }],
211
304
 
212
- // Sign the funded PSBT
213
- sign: ['fund', ({fund}, cbk) => {
214
- const [change] = fund.outputs.filter(n => !!n.is_change);
215
- const total = sumOf(fund.outputs.map(n => n.tokens));
216
-
217
- const tokens = !!change ? change.tokens : undefined;
305
+ // Final funded transaction
306
+ funded: ['getUtxos', 'sign', ({getUtxos, sign}, cbk) => {
307
+ // Match the inputs of the tx up to the wallet outputs
308
+ const tx = fromHex(sign.transaction);
309
+
310
+ // Find the UTXOs that are being spent in the final transaction
311
+ const spending = tx.ins.map(input => {
312
+ const outpoint = asOutpoint({
313
+ transaction_id: txHashAsTxId(input.hash),
314
+ transaction_vout: input.index,
315
+ });
218
316
 
219
- args.logger.info({
220
- change: !!tokens ? formatTokens({tokens}).display : undefined,
221
- sum_of_outputs: formatTokens({tokens: total}).display,
222
- spending_utxos: fund.inputs.map(asOutpoint),
317
+ return getUtxos.utxos.find(n => asOutpoint(n) === outpoint);
223
318
  });
224
319
 
225
- return signPsbt({lnd: args.lnd, psbt: fund.psbt}, cbk);
226
- }],
320
+ // Make sure the spending UTXOs are known
321
+ if (spending.filter(n => !n).length) {
322
+ return cbk([503, 'ExpectedSpendingKnownUtxosForFundedTx']);
323
+ }
227
324
 
228
- // Final funded transaction
229
- funded: ['sign', ({sign}, cbk) => {
230
- return cbk(null, {signed_transaction: sign.transaction});
325
+ const inputsValue = sumOf(spending.map(n => n.tokens));
326
+ const outputsValue = sumOf(tx.outs.map(n => n.value));
327
+
328
+ const feeTotal = inputsValue - outputsValue;
329
+
330
+ return cbk(null, {
331
+ fee_tokens_per_vbyte: formattedFeeRate(feeTotal / tx.virtualSize()),
332
+ signed_transaction: sign.transaction,
333
+ });
231
334
  }],
232
335
  },
233
336
  returnResult({reject, resolve, of: 'funded'}, cbk));
package/package.json CHANGED
@@ -36,7 +36,7 @@
36
36
  "invoices": "2.0.0",
37
37
  "ln-accounting": "5.0.3",
38
38
  "ln-service": "52.12.1",
39
- "ln-sync": "2.0.3",
39
+ "ln-sync": "2.1.0",
40
40
  "ln-telegram": "3.3.1",
41
41
  "moment": "2.29.1",
42
42
  "paid-services": "3.0.0",
@@ -80,5 +80,5 @@
80
80
  "postpublish": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t alexbosworth/balanceofsatoshis --push .",
81
81
  "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/fiat/*.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/wallets/*.js"
82
82
  },
83
- "version": "11.6.2"
83
+ "version": "11.7.0"
84
84
  }