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.
Files changed (52) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/capacity/accept_capacity_change.js +284 -0
  3. package/capacity/ask_for_change_details.js +505 -0
  4. package/capacity/ask_for_decrease.js +185 -0
  5. package/capacity/assemble_replacement_tx.js +171 -0
  6. package/capacity/capacity_change_requests.js +76 -0
  7. package/capacity/change_channel_capacity.js +183 -0
  8. package/capacity/encode_change_request.js +55 -0
  9. package/capacity/encode_sign_capacity_response.js +32 -0
  10. package/capacity/fee_for_replacement.js +108 -0
  11. package/capacity/finalize_capacity_replacement.js +67 -0
  12. package/capacity/get_capacity_change_requests.js +98 -0
  13. package/capacity/get_capacity_replacement.js +371 -0
  14. package/capacity/get_replacement_signature.js +232 -0
  15. package/capacity/index.js +4 -0
  16. package/capacity/initiate_capacity_change.js +506 -0
  17. package/capacity/interim_replacement_psbt.js +57 -0
  18. package/capacity/is_open_witness_script.js +40 -0
  19. package/capacity/parse_accept_request.js +58 -0
  20. package/capacity/parse_accept_response.js +52 -0
  21. package/capacity/parse_capacity_change_request.js +101 -0
  22. package/capacity/parse_sign_capacity_request.js +59 -0
  23. package/capacity/propose_capacity_change.js +361 -0
  24. package/capacity/service_types.json +6 -0
  25. package/capacity/sign_capacity_replacement.js +119 -0
  26. package/capacity/witness_script_from_close_txs.js +50 -0
  27. package/index.js +2 -0
  28. package/p2p/encode_peer_response.js +0 -1
  29. package/p2p/make_peer_request.js +3 -3
  30. package/package.json +6 -4
  31. package/test/capacity/test_assemble_replacement_tx.js +46 -0
  32. package/test/capacity/test_capacity_change_requests.js +54 -0
  33. package/test/capacity/test_encode_change_request.js +49 -0
  34. package/test/capacity/test_encode_sign_capacity_response.js +47 -0
  35. package/test/capacity/test_fee_for_replacement.js +40 -0
  36. package/test/capacity/test_finalize_capacity_replacement.js +45 -0
  37. package/test/capacity/test_interim_replacement_psbt.js +44 -0
  38. package/test/capacity/test_is_open_witness_script.js +40 -0
  39. package/test/capacity/test_parse_accept_request.js +42 -0
  40. package/test/capacity/test_parse_capacity_change_request.js +51 -0
  41. package/test/capacity/test_parse_sign_capacity_request.js +49 -0
  42. package/test/capacity/test_witness_script_from_close_txs.js +53 -0
  43. package/test/integration/test_accept_capacity_change.js +152 -0
  44. package/test/integration/test_change_channel_capacity.js +196 -0
  45. package/test/integration/test_decrease_channel_capacity.js +206 -0
  46. package/test/integration/test_increase_channel_capacity.js +197 -0
  47. package/test/integration/test_sign_capacity_replacement.js +142 -0
  48. package/test/p2p/test_encode_peer_request.js +53 -0
  49. package/test/p2p/test_encode_peer_response.js +38 -0
  50. package/test/p2p/test_encode_response_code.js +48 -0
  51. package/trades/network_from_network_record.js +6 -0
  52. package/trades/network_record_from_request.js +5 -0
@@ -0,0 +1,98 @@
1
+ const asyncAuto = require('async/auto');
2
+ const {getChannels} = require('ln-service');
3
+ const {returnResult} = require('asyncjs-util');
4
+ const {subscribeToPeerMessages} = require('ln-service');
5
+
6
+ const capacityChangeRequests = require('./capacity_change_requests');
7
+ const {serviceTypeChangeCapacity} = require('./service_types');
8
+ const parseCapacityChangeRequest = require('./parse_capacity_change_request');
9
+ const {servicePeerRequests} = require('./../p2p');
10
+
11
+ const defaultDelayMs = 1000 * 10;
12
+ const waitForRequestsTimeoutMs = 5000;
13
+
14
+ /** Get capacity change requests
15
+
16
+ {
17
+ [delay]: <Milliseconds To Wait For Change Requests Number>
18
+ lnd: <Authenticated LND API Object>
19
+ }
20
+
21
+ @returns via cbk or Promise
22
+ {
23
+ requests: [{
24
+ [address]: <Cooperative Close Address String>
25
+ capacity: <Channel Capacity Tokens Number>
26
+ channel: <Standard Format Channel Id String>
27
+ [decrease]: <Reduce Capacity Tokens Number>
28
+ from: <From Node Public Key Id Hex String>
29
+ id: <Change Request Id Hex String>
30
+ [increase]: <Add Capacity Tokens Number>
31
+ }]
32
+ }
33
+ */
34
+ module.exports = ({delay, lnd}, cbk) => {
35
+ return new Promise((resolve, reject) => {
36
+ return asyncAuto({
37
+ // Check arguments
38
+ validate: cbk => {
39
+ if (!lnd) {
40
+ return cbk([400, 'ExpectedLndToGetCapacityChangeRequests']);
41
+ }
42
+
43
+ return cbk();
44
+ },
45
+
46
+ // Wait for incoming capacity change requests
47
+ getRequests: ['validate', ({}, cbk) => {
48
+ const requests = [];
49
+ const service = servicePeerRequests({lnd});
50
+
51
+ // Listen for capacity change requests
52
+ service.request({type: serviceTypeChangeCapacity}, (req, res) => {
53
+ const {from, records} = req;
54
+
55
+ const {request} = parseCapacityChangeRequest({from, records});
56
+
57
+ // Exit early when the change request message was not understood
58
+ if (!request) {
59
+ return res.failure([400, 'FailedToParseChangeRequest']);
60
+ }
61
+
62
+ // Only add new requests
63
+ if (!requests.find(n => n.id === request.id)) {
64
+ requests.push(request);
65
+ }
66
+
67
+ return res.success({});
68
+ });
69
+
70
+ return setTimeout(() => {
71
+ // Stop listening for requests after a little while
72
+ service.stop({});
73
+
74
+ return cbk(null, requests);
75
+ },
76
+ delay || waitForRequestsTimeoutMs);
77
+ }],
78
+
79
+ // Get the set of channels
80
+ getChannels: ['getRequests', ({}, cbk) => getChannels({lnd}, cbk)],
81
+
82
+ // Final set of capacity change requests
83
+ requests: [
84
+ 'getChannels',
85
+ 'getRequests',
86
+ ({getChannels, getRequests}, cbk) =>
87
+ {
88
+ const {requests} = capacityChangeRequests({
89
+ channels: getChannels.channels,
90
+ requests: getRequests,
91
+ });
92
+
93
+ return cbk(null, {requests});
94
+ }],
95
+ },
96
+ returnResult({reject, resolve, of: 'requests'}, cbk));
97
+ });
98
+ };
@@ -0,0 +1,371 @@
1
+ const asyncAuto = require('async/auto');
2
+ const {closeChannel} = require('ln-service');
3
+ const {decodeChanId} = require('bolt07');
4
+ const {getChainFeeRate} = require('ln-service');
5
+ const {getChainTransactions} = require('ln-service');
6
+ const {getHeight} = require('ln-service');
7
+ const {getPendingChannels} = require('ln-service');
8
+ const {openChannels} = require('ln-service');
9
+ const {returnResult} = require('asyncjs-util');
10
+ const {Transaction} = require('bitcoinjs-lib');
11
+
12
+ const assembleReplacementTx = require('./assemble_replacement_tx');
13
+ const feeForReplacement = require('./fee_for_replacement');
14
+ const signCapacityReplacement = require('./sign_capacity_replacement');
15
+
16
+ const blocksBuffer = 2e3;
17
+ const bufferAsHex = buffer => buffer.toString('hex');
18
+ const defaultIncrease = 0;
19
+ const {fromHex} = Transaction;
20
+ const fuzzBlocks = 10;
21
+ const highValue = 3000;
22
+ const {isArray} = Array;
23
+ const maxCommitmentOutputs = 4;
24
+ const maxHighValueOutputs = 2;
25
+ const notFoundIndex = -1;
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 txIdAsHash = id => Buffer.from(id, 'hex').reverse();
30
+
31
+ /** Close a channel and get a capacity replacement transaction
32
+
33
+ {
34
+ [add_funds_transaction_id]: <Adding Funds Input Tx Id Hex Encoded String>
35
+ [add_funds_transaction_vout]: <Adding Funds Output Index Number>
36
+ bitcoinjs_network: <BitcoinJs Network Name String>
37
+ decrease: [{
38
+ address: <Spend to Chain Address String>
39
+ output: <Spend to Output Script Hex String>
40
+ tokens: <Spend Value Number>
41
+ }]
42
+ id: <Original Channel Standard Format Channel Id String>
43
+ [increase]: <Increase Channel Size Tokens Number>
44
+ [is_private]: <Original Channel Is Private Bool>
45
+ lnd: <Authenticated LND API Object>
46
+ transaction_id: <Original Funding Transaction Id Hex String>
47
+ transaction_vout: <Original Funding Output Index Number>
48
+ }
49
+
50
+ @returns via cbk or Promise
51
+ {
52
+ [add_funds_vin]: <Funding Spend Input Index Number>
53
+ capacity: <Replacement Channel Capacity Tokens Number>
54
+ remote_balance: <Initial Balance for Peer Tokens Number>
55
+ signature: <Signature for Replacement Transaction Hex Encoded String>
56
+ signing_key: <Signing Public Key Hex String>
57
+ pending_channel_id: <Proposed Channel Pending Id Hex String>
58
+ transaction_id: <Replacement Channel Transaction Id Hex String>
59
+ transaction_vin: <Original Channel Input Index Number>
60
+ transaction_vout: <Replacement Channel Transaction Output Index Number>
61
+ unsigned_transaction: <Unsigned Transaction Hex Encoded String>
62
+ witness_script: <Original Channel Witness Script Hex String>
63
+ }
64
+ */
65
+ module.exports = (args, cbk) => {
66
+ return new Promise((resolve, reject) => {
67
+ return asyncAuto({
68
+ // Check arguments
69
+ validate: cbk => {
70
+ if (!args.bitcoinjs_network) {
71
+ return cbk([400, 'ExpectedNetworkNameToGetCapacityReplacement']);
72
+ }
73
+
74
+ if (!isArray(args.decrease)) {
75
+ return cbk([400, 'ExpectedDecreaseArrayToGetCapacityReplacement']);
76
+ }
77
+
78
+ if (!args.lnd) {
79
+ return cbk([400, 'ExpectedLndApiToGetCapacityReplacement']);
80
+ }
81
+
82
+ if (!args.transaction_id) {
83
+ return cbk([400, 'ExpectedFundingTxIdToGetCapacityReplacement']);
84
+ }
85
+
86
+ if (args.transaction_vout === undefined) {
87
+ return cbk([400, 'ExpectedFundingOutIndexToGetCapacityReplacement']);
88
+ }
89
+
90
+ return cbk();
91
+ },
92
+
93
+ // Get the chain fee rate for a slow confirmation to calculate fee delta
94
+ getFeeRate: ['validate', ({}, cbk) => {
95
+ return getChainFeeRate({
96
+ confirmation_target: slowTarget,
97
+ lnd: args.lnd,
98
+ },
99
+ cbk);
100
+ }],
101
+
102
+ // Get the current chain height
103
+ getHeight: ['validate', ({}, cbk) => getHeight({lnd: args.lnd}, cbk)],
104
+
105
+ // Force close the channel in order to get a non-final sequence number
106
+ channelClose: ['getFeeRate', ({}, cbk) => {
107
+ return closeChannel({
108
+ lnd: args.lnd,
109
+ is_force_close: true,
110
+ transaction_id: args.transaction_id,
111
+ transaction_vout: args.transaction_vout,
112
+ },
113
+ cbk);
114
+ }],
115
+
116
+ // Get the pending channels
117
+ getPending: ['channelClose', ({}, cbk) => {
118
+ return getPendingChannels({lnd: args.lnd}, cbk);
119
+ }],
120
+
121
+ // Find the pending closing channel
122
+ pendingChannel: ['getPending', ({getPending}, cbk) => {
123
+ const channel = getPending.pending_channels.find(chan => {
124
+ if (chan.transaction_id !== args.transaction_id) {
125
+ return false;
126
+ }
127
+
128
+ return chan.transaction_vout === args.transaction_vout;
129
+ });
130
+
131
+ if (!channel) {
132
+ return cbk([503, 'FailedToFindPendingCloseChannelForReplacement']);
133
+ }
134
+
135
+ if (!!channel.pending_payments && !!channel.pending_payments.length) {
136
+ return cbk([503, 'ExpectedNoActiveHtlcsOnCapacityChangeChannel']);
137
+ }
138
+
139
+ return cbk(null, {
140
+ capacity: channel.capacity,
141
+ partner_public_key: channel.partner_public_key,
142
+ remote_balance: channel.remote_balance,
143
+ });
144
+ }],
145
+
146
+ // Get the chain transactions to pull out the raw close tx
147
+ getUpdatedTx: ['channelClose', 'getHeight', ({getHeight}, cbk) => {
148
+ return getChainTransactions({
149
+ after: getHeight.current_block_height - fuzzBlocks,
150
+ lnd: args.lnd,
151
+ },
152
+ cbk);
153
+ }],
154
+
155
+ // Find the force close transaction
156
+ closeTx: [
157
+ 'channelClose',
158
+ 'getUpdatedTx',
159
+ 'pendingChannel',
160
+ ({channelClose, getUpdatedTx, pendingChannel}, cbk) =>
161
+ {
162
+ const closeId = channelClose.transaction_id;
163
+ const id = args.transaction_id;
164
+ const {capacity} = pendingChannel;
165
+
166
+ const tx = getUpdatedTx.transactions.find(n => n.id === closeId);
167
+
168
+ if (!tx || !tx.transaction) {
169
+ return cbk([503, 'ExpectedCloseTransactionInTransactionsList']);
170
+ }
171
+
172
+ const commitment = fromHex(tx.transaction);
173
+
174
+ // Make sure there aren't unexpectedly too-many outputs
175
+ if (commitment.outs.length > maxCommitmentOutputs) {
176
+ return cbk([503, 'ExpectedFewerOutputsOnCommitmentTransaction']);
177
+ }
178
+
179
+ const highValueOuts = commitment.outs.filter(n => n.value > highValue);
180
+
181
+ // There shouldn't be many high value outputs
182
+ if (highValueOuts.length > maxHighValueOutputs) {
183
+ return cbk([500, 'ExpectedFewerHighValueOutputsOnCommitTx']);
184
+ }
185
+
186
+ // Input details for the commitment transaction
187
+ const [{hash, index, witness}, other] = commitment.ins;
188
+
189
+ // The commitment transaction should be spending the funding outpoint
190
+ if (!hash.equals(txIdAsHash(id)) || index !== args.transaction_vout) {
191
+ return cbk([503, 'ExpectedSpendingOpenTransactionOutputIndex']);
192
+ }
193
+
194
+ // There should be no other inputs on the closing tx
195
+ if (!!other) {
196
+ return cbk([503, 'ExpectedSingleInputForCloseTransaction']);
197
+ }
198
+
199
+ // The spending script is the final element in the witness
200
+ const [fundingScript] = witness.slice().reverse();
201
+
202
+ // The difference between commitment outputs and its input is the fee
203
+ const fee = capacity - sumOf(commitment.outs.map(n => n.value));
204
+
205
+ return cbk(null, {
206
+ fee,
207
+ id: commitment.getId(),
208
+ script: bufferAsHex(fundingScript),
209
+ transaction: tx.transaction,
210
+ weight: commitment.weight(),
211
+ });
212
+ }],
213
+
214
+ // Find the open transaction
215
+ openTx: ['getUpdatedTx', ({getUpdatedTx}, cbk) => {
216
+ const funding = fromHex(args.open_transaction);
217
+ const id = args.transaction_id;
218
+
219
+ if (!funding.outs[args.transaction_vout]) {
220
+ return cbk([503, 'ExpectedTransactionOutputOnFundingTransaction']);
221
+ }
222
+
223
+ const capacity = funding.outs[args.transaction_vout].value;
224
+ const output = bufferAsHex(funding.outs[args.transaction_vout].script);
225
+
226
+ return cbk(null, {capacity, output});
227
+ }],
228
+
229
+ // Calculate the new channel's capacity
230
+ newCapacity: [
231
+ 'closeTx',
232
+ 'getFeeRate',
233
+ 'pendingChannel',
234
+ ({closeTx, getFeeRate, pendingChannel}, cbk) =>
235
+ {
236
+ // The fee for the replacement tx must meet the relay rate minimum
237
+ const {fee} = feeForReplacement({
238
+ capacity: pendingChannel.capacity,
239
+ commit_transaction_fee: closeTx.fee,
240
+ commit_transaction_weight: closeTx.weight,
241
+ decrease: args.decrease,
242
+ increase: args.increase,
243
+ tokens_per_vbyte: getFeeRate.tokens_per_vbyte,
244
+ });
245
+
246
+ // The new capacity is capacity + increase - decreases - tx fee
247
+ const newCapacity = sumOf([
248
+ pendingChannel.capacity,
249
+ args.increase || defaultIncrease,
250
+ -sumOf(args.decrease.map(n => n.tokens)),
251
+ -fee,
252
+ ]);
253
+
254
+ return cbk(null, newCapacity);
255
+ }],
256
+
257
+ // Propose the new channel to replace the existing one
258
+ proposeChannel: [
259
+ 'newCapacity',
260
+ 'pendingChannel',
261
+ ({newCapacity, pendingChannel}, cbk) =>
262
+ {
263
+ return openChannels({
264
+ channels: [{
265
+ capacity: newCapacity,
266
+ give_tokens: pendingChannel.remote_balance,
267
+ partner_public_key: pendingChannel.partner_public_key,
268
+ }],
269
+ is_avoiding_broadcast: true,
270
+ is_private: args.is_private,
271
+ lnd: args.lnd,
272
+ },
273
+ cbk);
274
+ }],
275
+
276
+ // Put together the unsigned replacement transaction
277
+ replacementTx: [
278
+ 'closeTx',
279
+ 'newCapacity',
280
+ 'proposeChannel',
281
+ ({closeTx, newCapacity, proposeChannel}, cbk) =>
282
+ {
283
+ // The replacement tx pays to the proposed channel multisig address
284
+ const [{address}] = proposeChannel.pending;
285
+
286
+ try {
287
+ const replacement = assembleReplacementTx({
288
+ add_funds_transaction_id: args.add_funds_transaction_id,
289
+ add_funds_transaction_vout: args.add_funds_transaction_vout,
290
+ bitcoinjs_network: args.bitcoinjs_network,
291
+ close_transaction: closeTx.transaction,
292
+ decrease: args.decrease.map(decrease => ({
293
+ output: decrease.output,
294
+ tokens: decrease.tokens,
295
+ })),
296
+ funding_address: address,
297
+ funding_tokens: newCapacity,
298
+ transaction_id: args.transaction_id,
299
+ transaction_vout: args.transaction_vout,
300
+ });
301
+
302
+ return cbk(null, {
303
+ add_funds_vin: replacement.add_funds_vin,
304
+ transaction: replacement.transaction,
305
+ transaction_vin: replacement.transaction_vin,
306
+ transaction_id: replacement.transaction_id,
307
+ transaction_vout: replacement.transaction_vout,
308
+ });
309
+ } catch (err) {
310
+ return cbk([503, err.message]);
311
+ }
312
+ }],
313
+
314
+ // Partially sign the replacement transaction using the 2:2 key
315
+ signReplacement: [
316
+ 'closeTx',
317
+ 'openTx',
318
+ 'pendingChannel',
319
+ 'replacementTx',
320
+ ({closeTx, openTx, pendingChannel, replacementTx}, cbk) =>
321
+ {
322
+ return signCapacityReplacement({
323
+ capacity: openTx.capacity,
324
+ lnd: args.lnd,
325
+ output: openTx.output,
326
+ script: closeTx.script,
327
+ transaction: replacementTx.transaction,
328
+ vin: replacementTx.transaction_vin,
329
+ },
330
+ cbk);
331
+ }],
332
+
333
+ // Details of proposed replacement
334
+ proposedReplacement: [
335
+ 'closeTx',
336
+ 'newCapacity',
337
+ 'pendingChannel',
338
+ 'proposeChannel',
339
+ 'replacementTx',
340
+ 'signReplacement',
341
+ ({
342
+ closeTx,
343
+ newCapacity,
344
+ pendingChannel,
345
+ proposeChannel,
346
+ replacementTx,
347
+ signReplacement,
348
+ },
349
+ cbk) =>
350
+ {
351
+ const [pending] = proposeChannel.pending;
352
+
353
+ return cbk(null, {
354
+ add_funds_vin: replacementTx.add_funds_vin,
355
+ capacity: newCapacity,
356
+ force_close_tx_id: closeTx.id,
357
+ remote_balance: pendingChannel.remote_balance,
358
+ signature: signReplacement.signature,
359
+ signing_key: signReplacement.key,
360
+ pending_channel_id: pending.id,
361
+ transaction_id: replacementTx.transaction_id,
362
+ transaction_vin: replacementTx.transaction_vin,
363
+ transaction_vout: replacementTx.transaction_vout,
364
+ unsigned_transaction: replacementTx.transaction,
365
+ witness_script: closeTx.script,
366
+ });
367
+ }],
368
+ },
369
+ returnResult({reject, resolve, of: 'proposedReplacement'}, cbk));
370
+ });
371
+ };
@@ -0,0 +1,232 @@
1
+ const asyncAuto = require('async/auto');
2
+ const asyncRetry = require('async/retry');
3
+ const {closeChannel} = require('ln-service');
4
+ const {decodeChanId} = require('bolt07');
5
+ const {getChainTransactions} = require('ln-service');
6
+ const {getPendingChannels} = require('ln-service');
7
+ const {returnResult} = require('asyncjs-util');
8
+ const {Transaction} = require('bitcoinjs-lib');
9
+
10
+ const signCapacityReplacement = require('./sign_capacity_replacement');
11
+ const witnessScriptFromCloseTxs = require('./witness_script_from_close_txs');
12
+
13
+ const blocksBuffer = 1e3;
14
+ const {fromHex} = Transaction;
15
+ const interval = 200;
16
+ const notFoundIndex = -1;
17
+ const times = 72000;
18
+ const txIdAsHash = id => Buffer.from(id, 'hex').reverse();
19
+
20
+ /** Close channel and then sign the unsigned replacement transaction
21
+
22
+ {
23
+ channel: <Replace Channel Standard Format Id String>
24
+ id: <Channel Funding Transaction Id Hex String>
25
+ lnd: <Authenticated LND API Object>
26
+ output: <Original Funding Output Script Hex String>
27
+ unsigned: <Unsigned Replacement Transaction Hex String>
28
+ vout: <Channel Funding Output Index Number>
29
+ }
30
+
31
+ @returns via cbk or Promise
32
+ {
33
+ signature: <Funding Spend Signature Hex String>
34
+ transaction_id: <Replacement Transaction Id Hex String>
35
+ transaction_vout: <Replacement Transaction Output Index Number>
36
+ }
37
+ */
38
+ module.exports = ({channel, id, lnd, output, unsigned, vout}, cbk) => {
39
+ return new Promise((resolve, reject) => {
40
+ return asyncAuto({
41
+ // Check arguments
42
+ validate: cbk => {
43
+ if (!channel) {
44
+ return cbk([400, 'ExpectedChannelIdToSignReplacementTransaction']);
45
+ }
46
+
47
+ if (!id) {
48
+ return cbk([400, 'ExpectedFundingTxIdToSignReplacementTransaction']);
49
+ }
50
+
51
+ if (!lnd) {
52
+ return cbk([400, 'ExpectedLndToSignReplacementTransaction']);
53
+ }
54
+
55
+ if (!output) {
56
+ return cbk([400, 'ExpectedOutputScriptToGetReplacementSignature']);
57
+ }
58
+
59
+ if (!unsigned) {
60
+ return cbk([400, 'ExpectedUnsignedTxToGetReplacementSignature']);
61
+ }
62
+
63
+ if (vout === undefined) {
64
+ return cbk([400, 'ExpectedFundingOutputIndexToGetReplacementSig']);
65
+ }
66
+
67
+ return cbk();
68
+ },
69
+
70
+ // Find input index that is respending the original funding outpoint
71
+ vin: ['validate', ({}, cbk) => {
72
+ const inputIndex = fromHex(unsigned).ins.findIndex(input => {
73
+ return input.index === vout && input.hash.equals(txIdAsHash(id));
74
+ });
75
+
76
+ if (inputIndex === notFoundIndex) {
77
+ return cbk([503, 'ExpectedReplacementTransactionSpendingFunding']);
78
+ }
79
+
80
+ return cbk(null, inputIndex);
81
+ }],
82
+
83
+ // Force close the channel to allow for looking up the public key
84
+ closeChannel: ['vin', ({}, cbk) => {
85
+ return closeChannel({
86
+ lnd,
87
+ is_force_close: true,
88
+ transaction_id: id,
89
+ transaction_vout: vout,
90
+ },
91
+ cbk);
92
+ }],
93
+
94
+ // Get the pending close channels to see the final channel balance
95
+ getPending: ['closeChannel', ({}, cbk) => {
96
+ return getPendingChannels({lnd}, cbk);
97
+ }],
98
+
99
+ // Get the post close channel transactions to find the open/close txs
100
+ getTx: ['closeChannel', ({}, cbk) => {
101
+ const after = decodeChanId({channel}).block_height - blocksBuffer;
102
+
103
+ return getChainTransactions({after, lnd}, cbk);
104
+ }],
105
+
106
+ // Find the funding witness script in the close transaction
107
+ script: ['closeChannel', 'getTx', ({closeChannel, getTx}, cbk) => {
108
+ try {
109
+ const {script} = witnessScriptFromCloseTxs({
110
+ closing_tx_id: closeChannel.transaction_id,
111
+ transactions: getTx.transactions,
112
+ transaction_id: id,
113
+ transaction_vout: vout,
114
+ });
115
+
116
+ return cbk(null, script);
117
+ } catch (err) {
118
+ return cbk([503, err.message]);
119
+ }
120
+ }],
121
+
122
+ // Find the pending channel close that was just force closed
123
+ pending: [
124
+ 'closeChannel',
125
+ 'getPending',
126
+ ({closeChannel, getPending}, cbk) =>
127
+ {
128
+ // The pending close tx has the closing transaction id
129
+ const pending = getPending.pending_channels.find(chan => {
130
+ return chan.transaction_id === id && chan.transaction_vout === vout;
131
+ });
132
+
133
+ if (!pending) {
134
+ return cbk([503, 'ExpectedPendingChannelToGetReplacementSignature']);
135
+ }
136
+
137
+ // There should be no pending payments on the channel
138
+ if (!!pending.pending_payments && !!pending.pending_payments.length) {
139
+ return cbk([503, 'ExpectedNoActiveHtlcsOnCapacityChangeChannel']);
140
+ }
141
+
142
+ // Return the local balance expected to be returned in replacement
143
+ return cbk(null, {
144
+ capacity: pending.capacity,
145
+ local_balance: pending.local_balance,
146
+ partner_public_key: pending.partner_public_key,
147
+ });
148
+ }],
149
+
150
+ // Given the unsigned transaction, look for the matching open proposal
151
+ waitForProposal: ['pending', ({pending}, cbk) => {
152
+ const id = fromHex(unsigned).getId();
153
+
154
+ return asyncRetry({interval, times}, cbk => {
155
+ return getPendingChannels({lnd}, (err, res) => {
156
+ if (!!err) {
157
+ return cbk(err);
158
+ }
159
+
160
+ // Find an incoming channel that indicates it's safe to sign reopen
161
+ const incomingChannel = res.pending_channels.find(channel => {
162
+ // This will be an opening channel
163
+ if (!channel.is_opening) {
164
+ return false;
165
+ }
166
+
167
+ // The new channel must be from the same peer
168
+ if (channel.partner_public_key !== pending.partner_public_key) {
169
+ return false;
170
+ }
171
+
172
+ // The new channel must be spending the old transaction
173
+ if (channel.transaction_id !== id) {
174
+ return false;
175
+ }
176
+
177
+ // The new channel must have at least the balance of the old one
178
+ if (channel.local_balance < pending.local_balance) {
179
+ return false;
180
+ }
181
+
182
+ return true;
183
+ });
184
+
185
+ if (!incomingChannel) {
186
+ return cbk([503, 'FailedToFindPendingReplacementChannel']);
187
+ }
188
+
189
+ return cbk(null, {
190
+ transaction_id: incomingChannel.transaction_id,
191
+ transaction_vout: incomingChannel.transaction_vout,
192
+ });
193
+ });
194
+ },
195
+ cbk);
196
+ }],
197
+
198
+ // Sign the channel funding to prepare to authorize the new channel
199
+ signFunding: [
200
+ 'pending',
201
+ 'script',
202
+ 'vin',
203
+ 'waitForProposal',
204
+ ({pending, script, vin, waitForProposal}, cbk) =>
205
+ {
206
+ return signCapacityReplacement({
207
+ lnd,
208
+ output,
209
+ script,
210
+ vin,
211
+ capacity: pending.capacity,
212
+ transaction: unsigned,
213
+ },
214
+ cbk);
215
+ }],
216
+
217
+ // Final signature
218
+ signature: [
219
+ 'signFunding',
220
+ 'waitForProposal',
221
+ ({signFunding, waitForProposal}, cbk) =>
222
+ {
223
+ return cbk(null, {
224
+ signature: signFunding.signature,
225
+ transaction_id: waitForProposal.transaction_id,
226
+ transaction_vout: waitForProposal.transaction_vout,
227
+ });
228
+ }],
229
+ },
230
+ returnResult({reject, resolve, of: 'signature'}, cbk));
231
+ });
232
+ };
@@ -0,0 +1,4 @@
1
+ const changeChannelCapacity = require('./change_channel_capacity');
2
+ const getCapacityChangeRequests = require('./get_capacity_change_requests');
3
+
4
+ module.exports = {changeChannelCapacity, getCapacityChangeRequests};