paid-services 3.2.0 → 3.3.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/capacity/accept_capacity_change.js +284 -0
- package/capacity/ask_for_change_details.js +505 -0
- package/capacity/ask_for_decrease.js +185 -0
- package/capacity/assemble_replacement_tx.js +171 -0
- package/capacity/capacity_change_requests.js +76 -0
- package/capacity/change_channel_capacity.js +183 -0
- package/capacity/encode_change_request.js +55 -0
- package/capacity/encode_sign_capacity_response.js +32 -0
- package/capacity/fee_for_replacement.js +108 -0
- package/capacity/finalize_capacity_replacement.js +67 -0
- package/capacity/get_capacity_change_requests.js +98 -0
- package/capacity/get_capacity_replacement.js +371 -0
- package/capacity/get_replacement_signature.js +232 -0
- package/capacity/index.js +4 -0
- package/capacity/initiate_capacity_change.js +506 -0
- package/capacity/interim_replacement_psbt.js +57 -0
- package/capacity/is_open_witness_script.js +40 -0
- package/capacity/parse_accept_request.js +58 -0
- package/capacity/parse_accept_response.js +52 -0
- package/capacity/parse_capacity_change_request.js +101 -0
- package/capacity/parse_sign_capacity_request.js +59 -0
- package/capacity/propose_capacity_change.js +361 -0
- package/capacity/service_types.json +6 -0
- package/capacity/sign_capacity_replacement.js +119 -0
- package/capacity/witness_script_from_close_txs.js +50 -0
- package/index.js +2 -0
- package/p2p/encode_peer_response.js +0 -1
- package/p2p/make_peer_request.js +3 -3
- package/package.json +6 -4
- package/test/capacity/test_assemble_replacement_tx.js +46 -0
- package/test/capacity/test_capacity_change_requests.js +54 -0
- package/test/capacity/test_encode_change_request.js +49 -0
- package/test/capacity/test_encode_sign_capacity_response.js +47 -0
- package/test/capacity/test_fee_for_replacement.js +40 -0
- package/test/capacity/test_finalize_capacity_replacement.js +45 -0
- package/test/capacity/test_interim_replacement_psbt.js +44 -0
- package/test/capacity/test_is_open_witness_script.js +40 -0
- package/test/capacity/test_parse_accept_request.js +42 -0
- package/test/capacity/test_parse_capacity_change_request.js +51 -0
- package/test/capacity/test_parse_sign_capacity_request.js +49 -0
- package/test/capacity/test_witness_script_from_close_txs.js +53 -0
- package/test/integration/test_accept_capacity_change.js +152 -0
- package/test/integration/test_change_channel_capacity.js +196 -0
- package/test/integration/test_decrease_channel_capacity.js +206 -0
- package/test/integration/test_increase_channel_capacity.js +197 -0
- package/test/integration/test_sign_capacity_replacement.js +142 -0
- package/test/p2p/test_encode_peer_request.js +53 -0
- package/test/p2p/test_encode_peer_response.js +38 -0
- package/test/p2p/test_encode_response_code.js +48 -0
- package/trades/network_from_network_record.js +6 -0
- package/trades/network_record_from_request.js +5 -0
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
const asyncAuto = require('async/auto');
|
|
2
|
+
const asyncUntil = require('async/until');
|
|
3
|
+
const {findKey} = require('ln-sync');
|
|
4
|
+
const {getChainFeeRate} = require('ln-service');
|
|
5
|
+
const {getChainTransactions} = require('ln-service');
|
|
6
|
+
const {getChannel} = require('ln-service');
|
|
7
|
+
const {getChannels} = require('ln-service');
|
|
8
|
+
const {getPeers} = require('ln-service');
|
|
9
|
+
const {returnResult} = require('asyncjs-util');
|
|
10
|
+
const {sendMessageToPeer} = require('ln-service');
|
|
11
|
+
|
|
12
|
+
const askForDecrease = require('./ask_for_decrease');
|
|
13
|
+
const encodeChangeRequest = require('./encode_change_request');
|
|
14
|
+
const feeForReplacement = require('./fee_for_replacement');
|
|
15
|
+
|
|
16
|
+
const {ceil} = Math;
|
|
17
|
+
const dust = 550;
|
|
18
|
+
const dustBuffer = 750;
|
|
19
|
+
const isNumber = n => !isNaN(n);
|
|
20
|
+
const largeChannelsBit = 19;
|
|
21
|
+
const maxSats = 21e6 * 1e8;
|
|
22
|
+
const maxSize = 16777215;
|
|
23
|
+
const minNewLocalBalance = 0;
|
|
24
|
+
const nonNegative = n => Math.max(0, n);
|
|
25
|
+
const outputSize = 44;
|
|
26
|
+
const positive = n => Math.max(1, n);
|
|
27
|
+
const slowTarget = 1000;
|
|
28
|
+
const sumOf = arr => arr.reduce((sum, n) => sum + n, 0);
|
|
29
|
+
const sumOfTokens = arr => arr.reduce((sum, n) => sum + n.tokens, 0);
|
|
30
|
+
const testMessage = '00';
|
|
31
|
+
const tokensAsBigUnit = tokens => (tokens / 1e8).toFixed(8);
|
|
32
|
+
const weightAsVBytes = n => n / 4;
|
|
33
|
+
const weightBuffer = 150;
|
|
34
|
+
|
|
35
|
+
/** Ask for change details
|
|
36
|
+
|
|
37
|
+
{
|
|
38
|
+
ask: <Ask Function>
|
|
39
|
+
id: <Request Identifier Hex String>
|
|
40
|
+
lnd: <Authenticated LND API Object>
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@returns via cbk or Promise
|
|
44
|
+
{
|
|
45
|
+
base_fee_mtokens: <Base Fee Millitokens String>
|
|
46
|
+
cltv_delta: <Locktime Delta Number>
|
|
47
|
+
coop_close_address: <Cooperative Close Address String>
|
|
48
|
+
decrease: [{
|
|
49
|
+
address: <Decrease Address String>
|
|
50
|
+
output: <Output Script Hex String>
|
|
51
|
+
tokens: <Decrease Tokens Number>
|
|
52
|
+
}]
|
|
53
|
+
estimated_capacity: <Estimated Tokens For New Channel Capacity Number>
|
|
54
|
+
estimated_local_delta: <Estimated Local Balance Delta Tokens Number>
|
|
55
|
+
fee_rate: <Fees Charged in Millitokens Per Million Number>
|
|
56
|
+
id: <Standard Format Channel Id String>
|
|
57
|
+
increase: <Channel Capacity Increase Tokens Number>
|
|
58
|
+
is_private: <Channel is Private Bool>
|
|
59
|
+
[open_transaction]: <Channel Open Transaction Hex String>
|
|
60
|
+
partner_csv_delay: <Peer CSV Delay Number>
|
|
61
|
+
partner_public_key: <Node to Ask For Capacity Change Public Key Hex String>
|
|
62
|
+
records: [{
|
|
63
|
+
type: <Change Record Type Number String>
|
|
64
|
+
value: <Change Record Value Hex Encoded String>
|
|
65
|
+
}]
|
|
66
|
+
remote_balance: <Peer Balance Tokens Number>
|
|
67
|
+
transaction_id: <Channel Funding Transaction Id Hex String>
|
|
68
|
+
}
|
|
69
|
+
*/
|
|
70
|
+
module.exports = ({ask, id, lnd}, cbk) => {
|
|
71
|
+
return new Promise((resolve, reject) => {
|
|
72
|
+
return asyncAuto({
|
|
73
|
+
// Check arguments
|
|
74
|
+
validate: cbk => {
|
|
75
|
+
if (!ask) {
|
|
76
|
+
return cbk([400, 'ExpectedAskFunctionToAskForChangeDetails']);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (!id) {
|
|
80
|
+
return cbk([400, 'ExpectedRequestIdentifierToAskForChangeDetails']);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!lnd) {
|
|
84
|
+
return cbk([400, 'ExpectedLndToAskForChangeDetails']);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return cbk();
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
// Select peer to adjust
|
|
91
|
+
askForPeer: ['validate', ({}, cbk) => {
|
|
92
|
+
return ask({
|
|
93
|
+
name: 'query',
|
|
94
|
+
message: 'Public key or alias of peer to change capacity with?',
|
|
95
|
+
type: 'input',
|
|
96
|
+
validate: input => !!input,
|
|
97
|
+
},
|
|
98
|
+
({query}) => cbk(null, query));
|
|
99
|
+
}],
|
|
100
|
+
|
|
101
|
+
// Get channels to use to lookup a public key for a peer
|
|
102
|
+
getChannels: ['validate', ({}, cbk) => getChannels({lnd}, cbk)],
|
|
103
|
+
|
|
104
|
+
// Get feature info for peers to figure out maximum channel size
|
|
105
|
+
getFeatures: ['validate', ({}, cbk) => getPeers({lnd}, cbk)],
|
|
106
|
+
|
|
107
|
+
// Get the chain fee rate
|
|
108
|
+
getFeeRate: ['validate', ({}, cbk) => {
|
|
109
|
+
return getChainFeeRate({lnd, confirmation_target: slowTarget}, cbk);
|
|
110
|
+
}],
|
|
111
|
+
|
|
112
|
+
// Find the public key or alias entered
|
|
113
|
+
findKey: [
|
|
114
|
+
'askForPeer',
|
|
115
|
+
'getChannels',
|
|
116
|
+
({askForPeer, getChannels}, cbk) =>
|
|
117
|
+
{
|
|
118
|
+
return findKey({
|
|
119
|
+
lnd,
|
|
120
|
+
channels: getChannels.channels,
|
|
121
|
+
query: askForPeer,
|
|
122
|
+
},
|
|
123
|
+
cbk);
|
|
124
|
+
}],
|
|
125
|
+
|
|
126
|
+
// Send a test message to the selected peer to confirm they are connected
|
|
127
|
+
testPeer: ['findKey', 'getChannels', ({findKey, getChannels}, cbk) => {
|
|
128
|
+
const hasPeer = !!getChannels.channels.find(channel => {
|
|
129
|
+
return channel.partner_public_key === findKey.public_key;
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
if (!hasPeer) {
|
|
133
|
+
return cbk([400, 'UnknownPeerToChangeChannelCapacityWith']);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return sendMessageToPeer({
|
|
137
|
+
lnd,
|
|
138
|
+
message: testMessage,
|
|
139
|
+
public_key: findKey.public_key,
|
|
140
|
+
},
|
|
141
|
+
err => {
|
|
142
|
+
if (!!err) {
|
|
143
|
+
return cbk([503, 'CannotCommunicateWithSelectedPeer', {err}]);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return cbk();
|
|
147
|
+
});
|
|
148
|
+
}],
|
|
149
|
+
|
|
150
|
+
// Select a channel to adjust when there are multiple choices
|
|
151
|
+
askForChannel: [
|
|
152
|
+
'findKey',
|
|
153
|
+
'getChannels',
|
|
154
|
+
'testPeer',
|
|
155
|
+
({findKey, getChannels}, cbk) =>
|
|
156
|
+
{
|
|
157
|
+
const channelsForPeer = getChannels.channels.filter(channel => {
|
|
158
|
+
return channel.partner_public_key === findKey.public_key;
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const [channel, more] = channelsForPeer;
|
|
162
|
+
|
|
163
|
+
// Exit early when there is only one channel, no need to ask
|
|
164
|
+
if (!more) {
|
|
165
|
+
return cbk(null, channel.id);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const choices = channelsForPeer.map(channel => {
|
|
169
|
+
// Do not allow changes on inactive or coop close locked channels
|
|
170
|
+
const disabled = [
|
|
171
|
+
!!channel.cooperative_close_address,
|
|
172
|
+
!channel.is_active,
|
|
173
|
+
];
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
disabled: !!disabled.filter(n => !!n).length,
|
|
177
|
+
name: `${channel.id}: ${tokensAsBigUnit(channel.capacity)}`,
|
|
178
|
+
value: channel.id,
|
|
179
|
+
};
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
if (!choices.filter(n => !n.disabled).length) {
|
|
183
|
+
return cbk([400, 'NoSuitableChannelsToChangeCapacity']);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return ask({
|
|
187
|
+
choices,
|
|
188
|
+
loop: false,
|
|
189
|
+
message: 'Channel to change capacity?',
|
|
190
|
+
name: 'id',
|
|
191
|
+
type: 'list',
|
|
192
|
+
},
|
|
193
|
+
({id}) => cbk(null, id));
|
|
194
|
+
}],
|
|
195
|
+
|
|
196
|
+
// Get the channel policy info
|
|
197
|
+
getPolicy: ['askForChannel', ({askForChannel}, cbk) => {
|
|
198
|
+
return getChannel({lnd, id: askForChannel}, cbk);
|
|
199
|
+
}],
|
|
200
|
+
|
|
201
|
+
// Get chain transactions
|
|
202
|
+
getTx: ['askForChannel', ({askForChannel}, cbk) => {
|
|
203
|
+
return getChainTransactions({lnd}, cbk);
|
|
204
|
+
}],
|
|
205
|
+
|
|
206
|
+
// Details of the channel to change capacity with
|
|
207
|
+
channel: [
|
|
208
|
+
'askForChannel',
|
|
209
|
+
'findKey',
|
|
210
|
+
'getChannels',
|
|
211
|
+
'getFeatures',
|
|
212
|
+
'getFeeRate',
|
|
213
|
+
'getPolicy',
|
|
214
|
+
'getTx',
|
|
215
|
+
({
|
|
216
|
+
askForChannel,
|
|
217
|
+
findKey,
|
|
218
|
+
getChannels,
|
|
219
|
+
getFeatures,
|
|
220
|
+
getFeeRate,
|
|
221
|
+
getPolicy,
|
|
222
|
+
getTx,
|
|
223
|
+
},
|
|
224
|
+
cbk) =>
|
|
225
|
+
{
|
|
226
|
+
const feeRate = getFeeRate.tokens_per_vbyte;
|
|
227
|
+
const id = askForChannel;
|
|
228
|
+
const peerKey = findKey.public_key;
|
|
229
|
+
|
|
230
|
+
const channel = getChannels.channels.find(n => n.id === id);
|
|
231
|
+
|
|
232
|
+
// Do not allow selecting channels that are in use
|
|
233
|
+
if (channel.pending_payments.length) {
|
|
234
|
+
return cbk([400, 'ChannelHasPendingPayments']);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const peer = getFeatures.peers.find(n => n.public_key === peerKey);
|
|
238
|
+
|
|
239
|
+
// The peer must be connected
|
|
240
|
+
if (!peer) {
|
|
241
|
+
return cbk([503, 'FailedToFindConnectedPeer']);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const policy = getPolicy.policies.find(n => n.public_key === peerKey);
|
|
245
|
+
|
|
246
|
+
// A routing policy must be known so that it can be reset later
|
|
247
|
+
if (!policy || !policy.cltv_delta) {
|
|
248
|
+
return cbk([503, 'CannotFindChannelRoutingPolicyDetails']);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const openTx = getTx.transactions.find(tx => {
|
|
252
|
+
return tx.id === channel.transaction_id;
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// A peer that disallows large channels limits the increase allowable
|
|
256
|
+
const isLarge = peer.features.find(n => n.bit === largeChannelsBit);
|
|
257
|
+
|
|
258
|
+
// When decreasing, cannot decrease more than there are fund available
|
|
259
|
+
const maxDecreaseTokens = nonNegative(sumOf([
|
|
260
|
+
-channel.commit_transaction_fee,
|
|
261
|
+
-ceil(weightAsVBytes(weightBuffer) * feeRate),
|
|
262
|
+
-ceil(weightAsVBytes(channel.commit_transaction_weight) * feeRate),
|
|
263
|
+
channel.local_balance,
|
|
264
|
+
-dustBuffer,
|
|
265
|
+
]));
|
|
266
|
+
|
|
267
|
+
// When increasing, cannot increase more than the peer supports
|
|
268
|
+
const maxIncrease = !!isLarge ? maxSats : maxSize - channel.capacity;
|
|
269
|
+
|
|
270
|
+
return cbk(null, {
|
|
271
|
+
base_fee_mtokens: policy.base_fee_mtokens,
|
|
272
|
+
capacity: channel.capacity,
|
|
273
|
+
cltv_delta: policy.cltv_delta,
|
|
274
|
+
commit_transaction_fee: channel.commit_transaction_fee,
|
|
275
|
+
commit_transaction_weight: channel.commit_transaction_weight,
|
|
276
|
+
coop_close_address: channel.cooperative_close_address,
|
|
277
|
+
fee_rate: policy.fee_rate,
|
|
278
|
+
id: channel.id,
|
|
279
|
+
is_private: channel.is_private,
|
|
280
|
+
local_balance: channel.local_balance,
|
|
281
|
+
open_transaction: !!openTx ? openTx.transaction : undefined,
|
|
282
|
+
max_decrease_tokens: maxDecreaseTokens,
|
|
283
|
+
max_increase_tokens: nonNegative(maxIncrease),
|
|
284
|
+
partner_public_key: channel.partner_public_key,
|
|
285
|
+
remote_balance: channel.remote_balance,
|
|
286
|
+
remote_csv: channel.remote_csv,
|
|
287
|
+
transaction_id: channel.transaction_id,
|
|
288
|
+
transaction_vout: channel.transaction_vout,
|
|
289
|
+
});
|
|
290
|
+
}],
|
|
291
|
+
|
|
292
|
+
// Choose whether to add or remove coins
|
|
293
|
+
askForDirection: ['channel', ({channel}, cbk) => {
|
|
294
|
+
return ask({
|
|
295
|
+
choices: [
|
|
296
|
+
{
|
|
297
|
+
disabled: !channel.max_increase_tokens,
|
|
298
|
+
name: `Increase capacity (currently ${channel.capacity})`,
|
|
299
|
+
value: 'increase',
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
disabled: !channel.max_decrease_tokens,
|
|
303
|
+
name: `Decrease capacity (limit ${channel.max_decrease_tokens})`,
|
|
304
|
+
value: 'decrease',
|
|
305
|
+
},
|
|
306
|
+
],
|
|
307
|
+
message: 'How do you want to change the channel capacity?',
|
|
308
|
+
name: 'direction',
|
|
309
|
+
type: 'list',
|
|
310
|
+
},
|
|
311
|
+
({direction}) => cbk(null, direction));
|
|
312
|
+
}],
|
|
313
|
+
|
|
314
|
+
// Ask for how much to decrease
|
|
315
|
+
askForDecrease: [
|
|
316
|
+
'askForDirection',
|
|
317
|
+
'channel',
|
|
318
|
+
({askForDirection, channel}, cbk) =>
|
|
319
|
+
{
|
|
320
|
+
// Exit early when funds are being added
|
|
321
|
+
if (askForDirection !== 'decrease') {
|
|
322
|
+
return cbk();
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const decreases = [];
|
|
326
|
+
const maximum = channel.max_decrease_tokens;
|
|
327
|
+
|
|
328
|
+
if (!maximum) {
|
|
329
|
+
return cbk([400, 'ChannelLocalBalanceTooLowToDecrease']);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return asyncUntil(
|
|
333
|
+
cbk => cbk(null, !!decreases.find(n => n.is_final)),
|
|
334
|
+
cbk => {
|
|
335
|
+
return askForDecrease({
|
|
336
|
+
ask,
|
|
337
|
+
lnd,
|
|
338
|
+
max: maximum - sumOf(decreases.map(n => n.tokens + outputSize)),
|
|
339
|
+
},
|
|
340
|
+
(err, res) => {
|
|
341
|
+
if (!!err) {
|
|
342
|
+
return cbk(err);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (decreases.find(n => n.address === res.address)) {
|
|
346
|
+
return cbk([400, 'ExpectedUniqueAddressForSpend']);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return cbk(null, decreases.push(res));
|
|
350
|
+
});
|
|
351
|
+
},
|
|
352
|
+
err => {
|
|
353
|
+
if (!!err) {
|
|
354
|
+
return cbk(err);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const final = decreases
|
|
358
|
+
.filter(n => !!n.tokens)
|
|
359
|
+
.map(({address, output, tokens}) => ({address, output, tokens}));
|
|
360
|
+
|
|
361
|
+
return cbk(null, final);
|
|
362
|
+
}
|
|
363
|
+
);
|
|
364
|
+
}],
|
|
365
|
+
|
|
366
|
+
// Ask to see how much to add to the channel
|
|
367
|
+
askForIncrease: [
|
|
368
|
+
'askForDirection',
|
|
369
|
+
'channel',
|
|
370
|
+
({askForDirection, channel}, cbk) =>
|
|
371
|
+
{
|
|
372
|
+
// Exit early when funds are being added
|
|
373
|
+
if (askForDirection !== 'increase') {
|
|
374
|
+
return cbk();
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const balance = channel.local_balance;
|
|
378
|
+
|
|
379
|
+
return ask({
|
|
380
|
+
name: 'amount',
|
|
381
|
+
message: `Amount to add?`,
|
|
382
|
+
type: 'input',
|
|
383
|
+
validate: input => {
|
|
384
|
+
if (!isNumber(input) || !Number.isInteger(Number(input))) {
|
|
385
|
+
return false;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (!!Number(input) && Number(input) < dust) {
|
|
389
|
+
return false;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return true;
|
|
393
|
+
},
|
|
394
|
+
},
|
|
395
|
+
({amount}) => cbk(null, Number(amount)));
|
|
396
|
+
}],
|
|
397
|
+
|
|
398
|
+
// Estimate the chain fee
|
|
399
|
+
estimateChainFee: [
|
|
400
|
+
'askForDecrease',
|
|
401
|
+
'askForIncrease',
|
|
402
|
+
'channel',
|
|
403
|
+
'getFeeRate',
|
|
404
|
+
({askForDecrease, askForIncrease, channel, getFeeRate}, cbk) =>
|
|
405
|
+
{
|
|
406
|
+
const {fee} = feeForReplacement({
|
|
407
|
+
capacity: channel.capacity,
|
|
408
|
+
commit_transaction_fee: channel.commit_transaction_fee,
|
|
409
|
+
commit_transaction_weight: channel.commit_transaction_weight,
|
|
410
|
+
decrease: askForDecrease || [],
|
|
411
|
+
increase: askForIncrease || undefined,
|
|
412
|
+
tokens_per_vbyte: getFeeRate.tokens_per_vbyte,
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
return cbk(null, {fee});
|
|
416
|
+
}],
|
|
417
|
+
|
|
418
|
+
// Confirm chain fee payment estimate
|
|
419
|
+
confirmFeeEstimate: [
|
|
420
|
+
'channel',
|
|
421
|
+
'estimateChainFee',
|
|
422
|
+
({channel, estimateChainFee}, cbk) =>
|
|
423
|
+
{
|
|
424
|
+
const {fee} = estimateChainFee;
|
|
425
|
+
|
|
426
|
+
const pay = `Pay estimated on-chain channel replacement fee: ${fee}`;
|
|
427
|
+
|
|
428
|
+
return ask({
|
|
429
|
+
type: 'confirm',
|
|
430
|
+
name: 'proceed',
|
|
431
|
+
message: `${pay} for capacity change?`,
|
|
432
|
+
},
|
|
433
|
+
({proceed}) => cbk(null, proceed));
|
|
434
|
+
}],
|
|
435
|
+
|
|
436
|
+
// Final capacity change details
|
|
437
|
+
details: [
|
|
438
|
+
'askForDecrease',
|
|
439
|
+
'askForIncrease',
|
|
440
|
+
'channel',
|
|
441
|
+
'confirmFeeEstimate',
|
|
442
|
+
'estimateChainFee',
|
|
443
|
+
'getTx',
|
|
444
|
+
({
|
|
445
|
+
askForDecrease,
|
|
446
|
+
askForIncrease,
|
|
447
|
+
channel,
|
|
448
|
+
confirmFeeEstimate,
|
|
449
|
+
estimateChainFee,
|
|
450
|
+
getTx,
|
|
451
|
+
},
|
|
452
|
+
cbk) =>
|
|
453
|
+
{
|
|
454
|
+
if (!confirmFeeEstimate) {
|
|
455
|
+
return cbk([400, 'RejectedCapacityChangeDueToFeeEstimate']);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Estimate how much local balance will be adjusted down
|
|
459
|
+
const estimatedLocalDelta = sumOf([
|
|
460
|
+
-sumOfTokens(askForDecrease || []),
|
|
461
|
+
askForIncrease || Number(),
|
|
462
|
+
-estimateChainFee.fee,
|
|
463
|
+
]);
|
|
464
|
+
|
|
465
|
+
// The new capacity is the old one with decreases/increases, minus fee
|
|
466
|
+
const estimatedNewCapacity = sumOf([
|
|
467
|
+
channel.capacity,
|
|
468
|
+
estimatedLocalDelta,
|
|
469
|
+
]);
|
|
470
|
+
|
|
471
|
+
if (channel.local_balance + estimatedLocalDelta < minNewLocalBalance) {
|
|
472
|
+
return cbk([400, 'InsufficientFundsToChangeChannelCapacity']);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// The change will be communicated in change records
|
|
476
|
+
const {records} = encodeChangeRequest({
|
|
477
|
+
id,
|
|
478
|
+
channel: channel.id,
|
|
479
|
+
decrease: !!askForDecrease ? sumOfTokens(askForDecrease) : undefined,
|
|
480
|
+
increase: askForIncrease,
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
return cbk(null, {
|
|
484
|
+
records,
|
|
485
|
+
base_fee_mtokens: channel.base_fee_mtokens,
|
|
486
|
+
cltv_delta: channel.cltv_delta,
|
|
487
|
+
coop_close_address: channel.coop_close_address,
|
|
488
|
+
decrease: askForDecrease || [],
|
|
489
|
+
estimated_capacity: estimatedNewCapacity,
|
|
490
|
+
estimated_local_delta: estimatedLocalDelta,
|
|
491
|
+
fee_rate: channel.fee_rate,
|
|
492
|
+
id: channel.id,
|
|
493
|
+
increase: askForIncrease,
|
|
494
|
+
is_private: channel.is_private,
|
|
495
|
+
open_transaction: channel.open_transaction,
|
|
496
|
+
partner_csv_delay: channel.partner_csv_delay,
|
|
497
|
+
partner_public_key: channel.partner_public_key,
|
|
498
|
+
remote_balance: channel.remote_balance,
|
|
499
|
+
transaction_id: channel.transaction_id,
|
|
500
|
+
});
|
|
501
|
+
}],
|
|
502
|
+
},
|
|
503
|
+
returnResult({reject, resolve, of: 'details'}, cbk));
|
|
504
|
+
});
|
|
505
|
+
};
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
const {address} = require('bitcoinjs-lib');
|
|
2
|
+
const asyncAuto = require('async/auto');
|
|
3
|
+
const {createChainAddress} = require('ln-service');
|
|
4
|
+
const {getNetwork} = require('ln-sync');
|
|
5
|
+
const {networks} = require('bitcoinjs-lib');
|
|
6
|
+
const {returnResult} = require('asyncjs-util');
|
|
7
|
+
|
|
8
|
+
const bufferAsHex = buffer => buffer.toString('hex');
|
|
9
|
+
const dust = 550;
|
|
10
|
+
const isNumber = n => !isNaN(n);
|
|
11
|
+
const {toOutputScript} = address;
|
|
12
|
+
|
|
13
|
+
/** Ask for details about a decrease
|
|
14
|
+
|
|
15
|
+
{
|
|
16
|
+
ask: <Ask Function>
|
|
17
|
+
lnd: <Authenticated LND API Object>
|
|
18
|
+
max: <Maximum Possible Decrease Number>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@returns via cbk or Promise
|
|
22
|
+
{
|
|
23
|
+
[address]: <Send Funds to Address String>
|
|
24
|
+
is_final: <Decrease is Final Decrease Bool>
|
|
25
|
+
[output]: <Output Script Hex String>
|
|
26
|
+
tokens: <Withdraw Tokens Number>
|
|
27
|
+
}
|
|
28
|
+
*/
|
|
29
|
+
module.exports = ({ask, lnd, max}, cbk) => {
|
|
30
|
+
return new Promise((resolve, reject) => {
|
|
31
|
+
return asyncAuto({
|
|
32
|
+
// Check arguments
|
|
33
|
+
validate: cbk => {
|
|
34
|
+
if (!ask) {
|
|
35
|
+
return cbk([400, 'ExpectedAskFunctionToAskForDecrease']);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!lnd) {
|
|
39
|
+
return cbk([400, 'ExpectedAuthenticatedLndToAskForDecrease']);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!max) {
|
|
43
|
+
return cbk([400, 'ExpectedMaximumAvaialbleTok'])
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return cbk();
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
// Ask for the amount to decrease
|
|
50
|
+
askForAmount: ['validate', ({}, cbk) => {
|
|
51
|
+
return ask({
|
|
52
|
+
default: '0',
|
|
53
|
+
name: 'amount',
|
|
54
|
+
message: `How much do you want to spend out (max: ${max})?`,
|
|
55
|
+
type: 'input',
|
|
56
|
+
validate: input => {
|
|
57
|
+
if (!isNumber(input) || !Number.isInteger(Number(input))) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!!Number(input) && Number(input) < dust) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!!Number(input) && Number(input) > max) {
|
|
66
|
+
return `The maximum possible to decrease is ${max}`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return true;
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
({amount}) => cbk(null, amount));
|
|
73
|
+
}],
|
|
74
|
+
|
|
75
|
+
// Get network name
|
|
76
|
+
getNetwork: ['validate', ({}, cbk) => getNetwork({lnd}, cbk)],
|
|
77
|
+
|
|
78
|
+
// Ask if withdrawing to an external address
|
|
79
|
+
askForExternal: ['askForAmount', ({askForAmount}, cbk) => {
|
|
80
|
+
if (!Number(askForAmount)) {
|
|
81
|
+
return cbk();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return ask({
|
|
85
|
+
default: false,
|
|
86
|
+
name: 'spend',
|
|
87
|
+
message: `Spend ${askForAmount} to an external address?`,
|
|
88
|
+
type: 'confirm',
|
|
89
|
+
},
|
|
90
|
+
({spend}) => cbk(null, spend));
|
|
91
|
+
}],
|
|
92
|
+
|
|
93
|
+
// Ask for an external address
|
|
94
|
+
askForAddress: [
|
|
95
|
+
'askForAmount',
|
|
96
|
+
'askForExternal',
|
|
97
|
+
({askForAmount, askForExternal}, cbk) =>
|
|
98
|
+
{
|
|
99
|
+
// Exit early when the withdraw address is internal
|
|
100
|
+
if (askForExternal !== true) {
|
|
101
|
+
return cbk();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return ask({
|
|
105
|
+
name: 'address',
|
|
106
|
+
message: `Address to spend ${askForAmount} to?`,
|
|
107
|
+
type: 'input',
|
|
108
|
+
validate: input => !!input,
|
|
109
|
+
},
|
|
110
|
+
({address}) => cbk(null, {address}));
|
|
111
|
+
}],
|
|
112
|
+
|
|
113
|
+
// Ask if this is the last output
|
|
114
|
+
askForAddition: [
|
|
115
|
+
'askForAddress',
|
|
116
|
+
'askForExternal',
|
|
117
|
+
({askForExternal}, cbk) =>
|
|
118
|
+
{
|
|
119
|
+
// Exit early when the spend was to an internal addess
|
|
120
|
+
if (!askForExternal) {
|
|
121
|
+
return cbk();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return ask({
|
|
125
|
+
default: false,
|
|
126
|
+
name: 'add',
|
|
127
|
+
message: 'Add another spend to a different address?',
|
|
128
|
+
type: 'confirm',
|
|
129
|
+
},
|
|
130
|
+
({add}) => cbk(null, add));
|
|
131
|
+
}],
|
|
132
|
+
|
|
133
|
+
// Create a decrease address to withdraw funds out to
|
|
134
|
+
createAddress: ['askForExternal', ({askForExternal}, cbk) => {
|
|
135
|
+
// Exit early when the withdraw address is external
|
|
136
|
+
if (askForExternal !== false) {
|
|
137
|
+
return cbk();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return createChainAddress({lnd}, cbk);
|
|
141
|
+
}],
|
|
142
|
+
|
|
143
|
+
// Final decrease output details
|
|
144
|
+
decrease: [
|
|
145
|
+
'askForAddition',
|
|
146
|
+
'askForAddress',
|
|
147
|
+
'askForAmount',
|
|
148
|
+
'createAddress',
|
|
149
|
+
'getNetwork',
|
|
150
|
+
({
|
|
151
|
+
askForAddition,
|
|
152
|
+
askForAddress,
|
|
153
|
+
askForAmount,
|
|
154
|
+
createAddress,
|
|
155
|
+
getNetwork,
|
|
156
|
+
},
|
|
157
|
+
cbk) =>
|
|
158
|
+
{
|
|
159
|
+
const {address} = (createAddress || askForAddress || {});
|
|
160
|
+
|
|
161
|
+
// Exit early when not decreasing to an address
|
|
162
|
+
if (!address) {
|
|
163
|
+
return cbk(null, {is_final: true, tokens: Number(askForAmount)});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Make sure the address is valid
|
|
167
|
+
try {
|
|
168
|
+
toOutputScript(address, networks[getNetwork.bitcoinjs]);
|
|
169
|
+
} catch (err) {
|
|
170
|
+
return cbk([400, 'FailedToParseAddress', {err}]);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const output = toOutputScript(address, networks[getNetwork.bitcoinjs]);
|
|
174
|
+
|
|
175
|
+
return cbk(null, {
|
|
176
|
+
address,
|
|
177
|
+
is_final: !askForAddition,
|
|
178
|
+
output: bufferAsHex(output),
|
|
179
|
+
tokens: Number(askForAmount),
|
|
180
|
+
});
|
|
181
|
+
}],
|
|
182
|
+
},
|
|
183
|
+
returnResult({reject, resolve, of: 'decrease'}, cbk));
|
|
184
|
+
});
|
|
185
|
+
};
|