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 +4 -0
- package/bos +1 -0
- package/chain/fund_transaction.js +144 -41
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
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
|
|
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 (!
|
|
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:
|
|
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
|
|
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: [
|
|
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
|
|
248
|
+
const feeRate = args.fee_tokens_per_vbyte || getFee.tokens_per_vbyte;
|
|
172
249
|
|
|
173
|
-
if (!!
|
|
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
|
-
|
|
179
|
-
|
|
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
|
-
|
|
262
|
+
fee_tokens_per_vbyte: feeRate,
|
|
186
263
|
inputs: !!inputs.length ? inputs : undefined,
|
|
187
264
|
lnd: args.lnd,
|
|
188
|
-
|
|
265
|
+
outputs: finalOutputs,
|
|
189
266
|
},
|
|
190
267
|
cbk);
|
|
191
268
|
}],
|
|
192
269
|
|
|
193
|
-
//
|
|
194
|
-
|
|
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
|
-
//
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
|
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.
|
|
83
|
+
"version": "11.7.0"
|
|
84
84
|
}
|