paid-services 3.2.0 → 3.5.1

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 (75) hide show
  1. package/.prettierignore +2 -0
  2. package/CHANGELOG.md +13 -0
  3. package/capacity/accept_capacity_change.js +284 -0
  4. package/capacity/ask_for_change_details.js +538 -0
  5. package/capacity/ask_for_decrease.js +185 -0
  6. package/capacity/assemble_replacement_tx.js +171 -0
  7. package/capacity/capacity_change_requests.js +85 -0
  8. package/capacity/change_channel_capacity.js +189 -0
  9. package/capacity/encode_change_request.js +62 -0
  10. package/capacity/encode_sign_capacity_response.js +32 -0
  11. package/capacity/fee_for_replacement.js +108 -0
  12. package/capacity/finalize_capacity_replacement.js +67 -0
  13. package/capacity/get_capacity_change_requests.js +99 -0
  14. package/capacity/get_capacity_replacement.js +372 -0
  15. package/capacity/get_replacement_signature.js +232 -0
  16. package/capacity/index.js +4 -0
  17. package/capacity/initiate_capacity_change.js +507 -0
  18. package/capacity/interim_replacement_psbt.js +57 -0
  19. package/capacity/is_open_witness_script.js +40 -0
  20. package/capacity/parse_accept_request.js +58 -0
  21. package/capacity/parse_accept_response.js +52 -0
  22. package/capacity/parse_capacity_change_request.js +108 -0
  23. package/capacity/parse_sign_capacity_request.js +59 -0
  24. package/capacity/propose_capacity_change.js +364 -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/p2p/service_peer_requests.js +10 -2
  31. package/package.json +7 -4
  32. package/service_types.json +16 -0
  33. package/test/capacity/test_assemble_replacement_tx.js +46 -0
  34. package/test/capacity/test_capacity_change_requests.js +55 -0
  35. package/test/capacity/test_encode_change_request.js +54 -0
  36. package/test/capacity/test_encode_sign_capacity_response.js +47 -0
  37. package/test/capacity/test_fee_for_replacement.js +40 -0
  38. package/test/capacity/test_finalize_capacity_replacement.js +45 -0
  39. package/test/capacity/test_interim_replacement_psbt.js +44 -0
  40. package/test/capacity/test_is_open_witness_script.js +40 -0
  41. package/test/capacity/test_parse_accept_request.js +42 -0
  42. package/test/capacity/test_parse_capacity_change_request.js +52 -0
  43. package/test/capacity/test_parse_sign_capacity_request.js +49 -0
  44. package/test/capacity/test_witness_script_from_close_txs.js +53 -0
  45. package/test/integration/test_accept_capacity_change.js +189 -0
  46. package/test/integration/test_change_channel_capacity.js +203 -0
  47. package/test/integration/test_decrease_channel_capacity.js +216 -0
  48. package/test/integration/test_increase_channel_capacity.js +201 -0
  49. package/test/integration/test_sign_capacity_replacement.js +142 -0
  50. package/test/p2p/test_encode_peer_request.js +53 -0
  51. package/test/p2p/test_encode_peer_response.js +38 -0
  52. package/test/p2p/test_encode_response_code.js +48 -0
  53. package/test/trades/test_decode_basic_trade.js +40 -0
  54. package/test/trades/test_decode_open_trade.js +50 -0
  55. package/test/trades/test_encode_open_trade.js +35 -0
  56. package/test/trades/test_encode_trade.js +37 -0
  57. package/trades/accept_trade.js +52 -0
  58. package/trades/create_trade.js +124 -44
  59. package/trades/decode_basic_trade.js +45 -0
  60. package/trades/decode_open_trade.js +69 -0
  61. package/trades/decode_trade.js +48 -66
  62. package/trades/decode_trade_secret.js +89 -0
  63. package/trades/encode_open_trade.js +89 -0
  64. package/trades/encode_trade.js +48 -33
  65. package/trades/encode_trade_secret.js +50 -0
  66. package/trades/finalize_trade_secret.js +116 -0
  67. package/trades/find_trade.js +314 -0
  68. package/trades/manage_trade.js +46 -35
  69. package/trades/network_from_network_record.js +8 -2
  70. package/trades/network_record_from_network.js +36 -0
  71. package/trades/network_record_from_request.js +6 -0
  72. package/trades/node_as_record.js +98 -0
  73. package/trades/record_as_node.js +113 -0
  74. package/trades/request_trade_by_id.js +89 -0
  75. package/trades/service_trade_requests.js +184 -0
@@ -0,0 +1,52 @@
1
+ const {Transaction} = require('bitcoinjs-lib');
2
+
3
+ const findTxRecord = records => records.find(n => n.type === '1');
4
+ const {fromHex} = Transaction;
5
+
6
+ /** Parse channel capacity change accept response
7
+
8
+ {
9
+ records: [{
10
+ type: <Record Type Number String>
11
+ value: <Record Value Hex Encoded String>
12
+ }]
13
+ transaction_id: <Channel to Capacity Change Transaction Id Hex String>
14
+ }
15
+
16
+ @throws
17
+ <Error>
18
+
19
+ @returns
20
+ {
21
+ transaction: <Channel Open Transaction Hex String>
22
+ }
23
+ */
24
+ module.exports = args => {
25
+ // Exit early when there are no records
26
+ if (!args.records) {
27
+ return {};
28
+ }
29
+
30
+ const txRecord = findTxRecord(args.records);
31
+
32
+ // Exit early when there is no tx record
33
+ if (!txRecord) {
34
+ return {};
35
+ }
36
+
37
+ // Make sure that the tx record is a tx
38
+ try {
39
+ fromHex(txRecord.value);
40
+ } catch (err) {
41
+ throw new Error('ExpectedValidRawTransactionHexToParseAcceptResponse');
42
+ }
43
+
44
+ const txId = fromHex(txRecord.value).getId();
45
+
46
+ // Make sure that the tx matches the tx id
47
+ if (txId !== args.transaction_id) {
48
+ throw new Error('ExpectedTxRecordForRawChannelTransaction');
49
+ }
50
+
51
+ return {transaction: txRecord.value};
52
+ };
@@ -0,0 +1,108 @@
1
+ const {chanFormat} = require('bolt07');
2
+ const {decodeBigSize} = require('bolt01');
3
+
4
+ const channelIdHexLength = 16;
5
+ const decodeNumber = encoded => BigInt(decodeBigSize({encoded}).decoded);
6
+ const defaultRecord = {value: '00'};
7
+ const hexAsNumber = n => Buffer.from(n, 'hex').readUInt8();
8
+ const idHexLength = 64;
9
+ const tooLarge = BigInt(Number.MAX_SAFE_INTEGER);
10
+
11
+ const findChannelRecord = records => records.find(n => n.type === '2');
12
+ const findDecreaseRecord = records => records.find(n => n.type === '3');
13
+ const findIncreaseRecord = records => records.find(n => n.type === '4');
14
+ const findRequestIdRecord = records => records.find(n => n.type === '1');
15
+ const findTypeRecord = records => records.find(n => n.type === '5');
16
+ const findVersionRecord = records => records.find(n => n.type === '0');
17
+
18
+ /** Parse a capacity change request
19
+
20
+ {
21
+ from: <From Public Key Hex String>
22
+ [records]: [{
23
+ type: <Record Type Number String>
24
+ value: <Record Hex Encoded Value String>
25
+ }]
26
+ }
27
+
28
+ @returns
29
+ {
30
+ [request]: {
31
+ channel: <Standard Format Channel Id String>
32
+ [decrease]: <Reduce Capacity Tokens Number>
33
+ from: <From Node Public Key Id Hex String>
34
+ id: <Change Request Id Hex String>
35
+ [increase]: <Add Capacity Tokens Number>
36
+ [type]: <Intended Replacement Channel Channel Type Flags Number>
37
+ }
38
+ }
39
+ */
40
+ module.exports = ({from, records}) => {
41
+ // Exit early when there are no records
42
+ if (!records) {
43
+ return {};
44
+ }
45
+
46
+ // Exit early when there is a version record, indicating a future version
47
+ if (!!findVersionRecord(records)) {
48
+ return {};
49
+ }
50
+
51
+ const channelRecord = findChannelRecord(records);
52
+
53
+ // Exit early when there is no channel
54
+ if (!channelRecord || channelRecord.value.length !== channelIdHexLength) {
55
+ return {};
56
+ }
57
+
58
+ // Make sure the channel id is a regular one
59
+ try {
60
+ chanFormat({id: channelRecord.value});
61
+ } catch (err) {
62
+ return {};
63
+ }
64
+
65
+ const idRecord = findRequestIdRecord(records);
66
+
67
+ // Exit early when there is no request id
68
+ if (!idRecord || idRecord.value.length !== idHexLength) {
69
+ return {};
70
+ }
71
+
72
+ const decreaseRecord = findDecreaseRecord(records);
73
+ const increaseRecord = findIncreaseRecord(records);
74
+ const typeRecord = findTypeRecord(records);
75
+
76
+ // Exit early when there is a change in both directions
77
+ if (!!decreaseRecord && !!increaseRecord) {
78
+ return {};
79
+ }
80
+
81
+ const {channel} = chanFormat({id: channelRecord.value});
82
+ const id = idRecord.value;
83
+ const type = typeRecord ? hexAsNumber(typeRecord.value) : undefined;
84
+
85
+ // Exit early when there is no decrease or increase
86
+ if (!decreaseRecord && !increaseRecord) {
87
+ return {request: {channel, from, id, type}};
88
+ }
89
+
90
+ const decrease = decodeNumber((decreaseRecord || defaultRecord).value);
91
+ const increase = decodeNumber((increaseRecord || defaultRecord).value);
92
+
93
+ // Exit early when there is no reasonably sized increase or decrease
94
+ if (decrease > tooLarge || increase > tooLarge) {
95
+ return {};
96
+ }
97
+
98
+ return {
99
+ request: {
100
+ channel,
101
+ from,
102
+ id,
103
+ type,
104
+ decrease: Number(decrease) || undefined,
105
+ increase: Number(increase) || undefined,
106
+ },
107
+ };
108
+ };
@@ -0,0 +1,59 @@
1
+ const {Transaction} = require('bitcoinjs-lib');
2
+
3
+ const {fromHex} = Transaction;
4
+ const findTransactionRecord = records => records.find(n => n.type === '1');
5
+ const txIdAsHash = id => Buffer.from(id, 'hex').reverse();
6
+
7
+ /** Parse request records requesting signing a capacity change request
8
+
9
+ {
10
+ id: <Funding Transaction Id Hex String>
11
+ increase: <Increase By Tokens Number String>
12
+ records: [{
13
+ type: <Record Type Number String>
14
+ value: <Record Value Hex Encoded String>
15
+ }]
16
+ vout: <Funding Transaction Output Index Number>
17
+ }
18
+
19
+ @throws
20
+ <Error>
21
+
22
+ @returns
23
+ {
24
+ unsigned: <Unsigned Transaction Replacement Hex String>
25
+ }
26
+ */
27
+ module.exports = ({id, increase, records, vout}) => {
28
+ const txRecord = findTransactionRecord(records);
29
+
30
+ if (!txRecord) {
31
+ throw new Error('ExpectedTransactionRecordInSignCapacityChangeRequest');
32
+ }
33
+
34
+ // Check that the unsigned tx is a tx
35
+ try {
36
+ fromHex(txRecord.value);
37
+ } catch (err) {
38
+ throw new Error('FailedToParseSignCapacityChangeRequestTransaction');
39
+ }
40
+
41
+ const hash = txIdAsHash(id);
42
+ const tx = fromHex(txRecord.value);
43
+
44
+ const fundingSpend = tx.ins.filter(input => {
45
+ return input.index === vout && input.hash.equals(hash);
46
+ });
47
+
48
+ // Make sure that the tx spends the funding tx outpoint
49
+ if (!fundingSpend.length) {
50
+ throw new Error('ExpectedSpendOfFundingTransactionInChangeRequestTx');
51
+ }
52
+
53
+ // Make sure that an increase has an additional input
54
+ if (!!increase && tx.ins.length === [fundingSpend].length) {
55
+ throw new Error('ExpectedAddedInputInChangeTxForChangeCapacityIncrease');
56
+ }
57
+
58
+ return {unsigned: txRecord.value};
59
+ };
@@ -0,0 +1,364 @@
1
+ const asyncAuto = require('async/auto');
2
+ const asyncDoUntil = require('async/doUntil');
3
+ const asyncReflect = require('async/reflect');
4
+ const {broadcastChainTransaction} = require('ln-service');
5
+ const {cancelPendingChannel} = require('ln-service');
6
+ const {fundPendingChannels} = require('ln-service');
7
+ const {getChainTransactions} = require('ln-service');
8
+ const {getChannels} = require('ln-service');
9
+ const {getClosedChannels} = require('ln-service');
10
+ const {getHeight} = require('ln-service');
11
+ const {returnResult} = require('asyncjs-util');
12
+ const {signTransaction} = require('ln-service');
13
+ const {Transaction} = require('bitcoinjs-lib');
14
+ const {transactionAsPsbt} = require('psbt');
15
+
16
+ const finalizeCapacityReplacement = require('./finalize_capacity_replacement');
17
+ const getCapacityReplacement = require('./get_capacity_replacement');
18
+ const interimReplacementPsbt = require('./interim_replacement_psbt');
19
+ const {makePeerRequest} = require('./../p2p');
20
+ const {serviceTypeSignCapacityChange} = require('./../service_types');
21
+
22
+ const capacityChangeIdType = '0';
23
+ const findSignatureRecord = records => records.find(n => n.type === '1');
24
+ const fuzzHeight = 3;
25
+ const maxSigHexLength = 146;
26
+ const peerRequestTimeoutMs = 1000 * 60 * 10;
27
+ const rebroadcastDelayMs = 1000 * 3;
28
+ const {SIGHASH_ALL} = Transaction;
29
+ const transitFamily = 805;
30
+ const unsignedTransactionType = '1';
31
+
32
+ /** Propose the capacity change to be accepted
33
+
34
+ {
35
+ bitcoinjs_network: <BitcoinJS Network Name String>
36
+ channel: <Channel to Change Standard Format Channel Id 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: <Capacity Change Request Id Hex String>
43
+ is_private: <Channel is Private Bool>
44
+ [increase]: <Increase Channel Size Tokens Number>
45
+ [increase_key_index]: <Increase Funds Transit Key Number>
46
+ [increase_output_script]: <Increase Funds Output Script Hex String>
47
+ [increase_transaction_id]: <Increase Funds Outpoint Tx Id Hex String>
48
+ [increase_transaction_vout]: <Increase Funds Outpoint Output Index Number>
49
+ [increase_witness_script]: <Increase Funds Witness Script Hex String>
50
+ lnd: <Authenticated LND API Object>
51
+ logger: <Winston Logger Object>
52
+ open_transaction: <Original Channel Funding Transaction Hex String>
53
+ partner_public_key: <Peer Public Key Hex Encoded String>
54
+ transaction_id: <Original Channel Transaction Id Hex String>
55
+ transaction_vout: <Original Channel Transaction Output Index Number>
56
+ }
57
+
58
+ @returns via cbk or Promise
59
+ {
60
+ transaction_id: <Replacement Channel Transaction Id Hex String>
61
+ transaction_vout: <Replacement Channel Transaction Output Index Number>
62
+ }
63
+ */
64
+ module.exports = (args, cbk) => {
65
+ return new Promise((resolve, reject) => {
66
+ return asyncAuto({
67
+ // Check arguments
68
+ validate: cbk => {
69
+ if (!args.bitcoinjs_network) {
70
+ return cbk([400, 'ExpectedBitcoinjsNetworkNameToProposeChange']);
71
+ }
72
+
73
+ if (!args.channel) {
74
+ return cbk([400, 'ExpectedChannelIdToProposeCapacityChangeFor']);
75
+ }
76
+
77
+ if (!args.decrease) {
78
+ return cbk([400, 'ExpectedDecreaseSetToProposeCapacityChange']);
79
+ }
80
+
81
+ if (!args.id) {
82
+ return cbk([400, 'ExpectedChangeRequestIdToProposeCapacityChange']);
83
+ }
84
+
85
+ if (!args.lnd) {
86
+ return cbk([400, 'ExpectedAuthenticatedLndToProposeCapacityChange']);
87
+ }
88
+
89
+ if (!args.logger) {
90
+ return cbk([400, 'ExpectedWinstonLoggerToProposeCapacityChange']);
91
+ }
92
+
93
+ if (!args.open_transaction) {
94
+ return cbk([400, 'ExpectedChannelOpenTxToProposeCapacityChange']);
95
+ }
96
+
97
+ if (!args.partner_public_key) {
98
+ return cbk([400, 'ExpectedPeerNodeIdToProposeCapacityChange']);
99
+ }
100
+
101
+ if (!args.transaction_id) {
102
+ return cbk([400, 'ExpectedOriginalTxIdToProposeCapacityChange']);
103
+ }
104
+
105
+ if (args.transaction_vout === undefined) {
106
+ return cbk([400, 'ExpectedOriginalChannelTxOutputIndexToPropose']);
107
+ }
108
+
109
+ return cbk();
110
+ },
111
+
112
+ // Get the starting height
113
+ getHeight: ['validate', ({}, cbk) => getHeight({lnd: args.lnd}, cbk)],
114
+
115
+ // Close the current channal and get the unsigned replacement
116
+ getReplacement: ['validate', ({}, cbk) => {
117
+ return getCapacityReplacement({
118
+ add_funds_transaction_id: args.increase_transaction_id,
119
+ add_funds_transaction_vout: args.increase_transaction_vout,
120
+ bitcoinjs_network: args.bitcoinjs_network,
121
+ decrease: args.decrease,
122
+ id: args.channel,
123
+ is_private: args.is_private,
124
+ increase: args.increase,
125
+ is_private: args.is_private,
126
+ lnd: args.lnd,
127
+ open_transaction: args.open_transaction,
128
+ transaction_id: args.transaction_id,
129
+ transaction_vout: args.transaction_vout,
130
+ },
131
+ cbk);
132
+ }],
133
+
134
+ // Replacement channel outpoint
135
+ replacement: ['getReplacement', ({getReplacement}, cbk) => {
136
+ args.logger.info({pending_chan_id: getReplacement.pending_channel_id});
137
+
138
+ return cbk(null, {
139
+ transaction_id: getReplacement.transaction_id,
140
+ transaction_vout: getReplacement.transaction_vout,
141
+ });
142
+ }],
143
+
144
+ // Partially sign the replacement transaction using the add funds key
145
+ signAddFunds: ['getReplacement', ({getReplacement}, cbk) => {
146
+ // Exit early when there are no additional funds added
147
+ if (!args.increase) {
148
+ return cbk(null, {});
149
+ }
150
+
151
+ return signTransaction({
152
+ inputs: [{
153
+ key_family: transitFamily,
154
+ key_index: args.increase_key_index,
155
+ output_script: args.increase_output_script,
156
+ output_tokens: args.increase,
157
+ sighash: SIGHASH_ALL,
158
+ vin: getReplacement.add_funds_vin,
159
+ witness_script: args.increase_witness_script,
160
+ }],
161
+ lnd: args.lnd,
162
+ transaction: getReplacement.unsigned_transaction,
163
+ },
164
+ cbk);
165
+ }],
166
+
167
+ // Use the unsigned replacement transaction to "fund" the pending channel
168
+ fundWithReplacement: [
169
+ 'getReplacement',
170
+ 'signAddFunds',
171
+ ({getReplacement, signAddFunds}, cbk) =>
172
+ {
173
+ const [addFundsSignature] = signAddFunds.signatures || [];
174
+
175
+ const {psbt} = interimReplacementPsbt({
176
+ increase_public_key: args.increase_key,
177
+ increase_signature: addFundsSignature,
178
+ increase_transaction: args.increase_transaction,
179
+ increase_transaction_vin: getReplacement.add_funds_vin,
180
+ open_transaction: args.open_transaction,
181
+ signature: getReplacement.signature,
182
+ unsigned_transaction: getReplacement.unsigned_transaction,
183
+ witness_script: getReplacement.witness_script,
184
+ });
185
+
186
+ // Create the commitment transaction with the peer on the replacement
187
+ return fundPendingChannels({
188
+ channels: [getReplacement.pending_channel_id],
189
+ funding: psbt,
190
+ lnd: args.lnd,
191
+ },
192
+ cbk);
193
+ }],
194
+
195
+ // Send the final go ahead signal now that a proposal is made
196
+ getPeerSignature: [
197
+ 'getReplacement',
198
+ 'fundWithReplacement',
199
+ asyncReflect(({getReplacement}, cbk) =>
200
+ {
201
+ return makePeerRequest({
202
+ lnd: args.lnd,
203
+ records: [
204
+ {
205
+ type: unsignedTransactionType,
206
+ value: getReplacement.unsigned_transaction,
207
+ },
208
+ {
209
+ type: capacityChangeIdType,
210
+ value: args.id,
211
+ },
212
+ ],
213
+ timeout: peerRequestTimeoutMs,
214
+ to: args.partner_public_key,
215
+ type: serviceTypeSignCapacityChange,
216
+ },
217
+ (err, res) => {
218
+ if (!!err) {
219
+ return cbk(err);
220
+ }
221
+
222
+ if (!res.records) {
223
+ return cbk([503, 'ExpectedSignatureResponseFromPeerForChange']);
224
+ }
225
+
226
+ const record = findSignatureRecord(res.records);
227
+
228
+ if (!record || record.value.length > maxSigHexLength) {
229
+ return cbk([503, 'ExpectedSignatureRecordFromPeerForChange']);
230
+ }
231
+
232
+ return cbk(null, record.value);
233
+ });
234
+ })],
235
+
236
+ // Cancel the funding proposal when there is an error
237
+ cancelFunding: [
238
+ 'getPeerSignature',
239
+ 'getReplacement',
240
+ ({getPeerSignature, getReplacement}, cbk) =>
241
+ {
242
+ // Exit early when there is no need to cancel
243
+ if (!getPeerSignature.error) {
244
+ return cbk();
245
+ }
246
+
247
+ return cancelPendingChannel({
248
+ id: getReplacement.pending_channel_id,
249
+ lnd: args.lnd,
250
+ },
251
+ err => {
252
+ if (!!err) {
253
+ return cbk([503, 'FailedToCancelPendingChannel', {err}]);
254
+ }
255
+
256
+ // Return the original error
257
+ return cbk(getPeerSignature.error);
258
+ });
259
+ }],
260
+
261
+ // Broadcast the new funding transaction using the peer's signature
262
+ confirmFunding: [
263
+ 'getHeight',
264
+ 'getPeerSignature',
265
+ 'getReplacement',
266
+ 'signAddFunds',
267
+ ({getHeight, getPeerSignature, getReplacement, signAddFunds}, cbk) =>
268
+ {
269
+ // Exit early when there was an error getting the peer signature
270
+ if (!!getPeerSignature.error) {
271
+ return cbk();
272
+ }
273
+
274
+ const [addFundsSignature] = signAddFunds.signatures || [];
275
+
276
+ // Insert the peer signature to get the final signed funding tx
277
+ const {transaction} = finalizeCapacityReplacement({
278
+ add_funds_vin: getReplacement.add_funds_vin,
279
+ add_funds_public_key: args.increase_key,
280
+ add_funds_signature: addFundsSignature,
281
+ local_public_key: getReplacement.signing_key,
282
+ local_signature: getReplacement.signature,
283
+ funding_spend_vin: getReplacement.transaction_vin,
284
+ remote_signature: getPeerSignature.value,
285
+ transaction: getReplacement.unsigned_transaction,
286
+ witness_script: getReplacement.witness_script,
287
+ });
288
+
289
+ args.logger.info({publishing_new_channel_transaction: transaction});
290
+
291
+ return asyncDoUntil(
292
+ cbk => {
293
+ // Try and override the force close tx using the replacement
294
+ return broadcastChainTransaction({
295
+ transaction,
296
+ after: getHeight.current_block_height - fuzzHeight,
297
+ lnd: args.lnd,
298
+ },
299
+ cbk);
300
+ },
301
+ (broadcast, cbk) => {
302
+ args.logger.info({waiting_for_confirmation: broadcast.id});
303
+
304
+ // Rebroadcast until the transaction is confirmed
305
+ return getChainTransactions({lnd: args.lnd}, (err, res) => {
306
+ if (!!err) {
307
+ return cbk(err);
308
+ }
309
+
310
+ const confirmed = res.transactions.filter(n => !!n.is_confirmed);
311
+
312
+ // Find the confirmed tx
313
+ const tx = confirmed.find(tx => tx.id === broadcast.id);
314
+
315
+ if (!!tx) {
316
+ return cbk(null, true);
317
+ }
318
+
319
+ // Find the replacing tx
320
+ const replacing = confirmed.find(tx => {
321
+ return tx.id === getReplacement.force_close_tx_id;
322
+ });
323
+
324
+ // Cancel the pending channel if the force close confirms
325
+ if (!!replacing) {
326
+ return cancelPendingChannel({
327
+ id: getReplacement.pending_channel_id,
328
+ lnd: args.lnd,
329
+ },
330
+ () => {
331
+ return cbk([503, 'FailedToReplaceForceCloseTransaction']);
332
+ });
333
+ }
334
+
335
+ return getChannels({
336
+ lnd: args.lnd,
337
+ partner_public_key: args.partner_public_key,
338
+ },
339
+ (err, res) => {
340
+ if (!!err) {
341
+ return cbk(err);
342
+ }
343
+
344
+ const channel = res.channels.find(channel => {
345
+ return channel.transaction_id === broadcast.id
346
+ });
347
+
348
+ // Stop broadcasting when a channel shows up
349
+ if (!!channel) {
350
+ return cbk(null, true);
351
+ }
352
+
353
+ // Rebroadcast after a delay
354
+ return setTimeout(() => cbk(null, false), rebroadcastDelayMs);
355
+ });
356
+ });
357
+ },
358
+ cbk
359
+ );
360
+ }],
361
+ },
362
+ returnResult({reject, resolve, of: 'replacement'}, cbk));
363
+ });
364
+ };
@@ -0,0 +1,119 @@
1
+ const asyncAuto = require('async/auto');
2
+ const asyncDetectLimit = require('async/detectLimit');
3
+ const {getPublicKey} = require('ln-service');
4
+ const {returnResult} = require('asyncjs-util');
5
+ const {script} = require('bitcoinjs-lib');
6
+ const {signTransaction} = require('ln-service');
7
+ const {Transaction} = require('bitcoinjs-lib');
8
+
9
+ const bufferAsHex = buffer => buffer.toString('hex');
10
+ const {decompile} = script;
11
+ const family = 0;
12
+ const hexAsBuffer = hex => Buffer.from(hex, 'hex');
13
+ const {isBuffer} = Buffer;
14
+ const limit = 250;
15
+ const multiKeyLimit = 100000;
16
+ const range = max => [...Array(max).keys()];
17
+ const {SIGHASH_ALL} = Transaction;
18
+
19
+ /** Sign a capacity replacement given a witness for a channel close
20
+
21
+ {
22
+ capacity: <Original Channel Capacity Tokens Number>
23
+ lnd: <Authenticated LND API Object>
24
+ output: <Channel Funding Output Script Hex String>
25
+ script: <Witness Script String>
26
+ transaction: <Unsigned Transaction Hex String>
27
+ vin: <Replacement Transaction Input Index Number>
28
+ }
29
+
30
+ @returns via cbk or Promise
31
+ {
32
+ key: <Public Key Hex String>
33
+ signature: <Signature Hex String>
34
+ }
35
+ */
36
+ module.exports = ({capacity, lnd, output, script, transaction, vin}, cbk) => {
37
+ return new Promise((resolve, reject) => {
38
+ return asyncAuto({
39
+ // Check arguments
40
+ validate: cbk => {
41
+ if (!capacity) {
42
+ return cbk([400, 'ExpectedCapacityToSignCapacityReplacementTx']);
43
+ }
44
+
45
+ if (!lnd) {
46
+ return cbk([400, 'ExpectedLndToSignCapacityChangeReplacementTx']);
47
+ }
48
+
49
+ if (!output) {
50
+ return cbk([400, 'ExpectedOutputScriptToSignCapacityChangeTx']);
51
+ }
52
+
53
+ if (!script) {
54
+ return cbk([400, 'ExpectedWitnessScriptToSignReplacementTx']);
55
+ }
56
+
57
+ if (!transaction) {
58
+ return cbk([400, 'ExpectedUnsignedTransactionToSignReplacementTx']);
59
+ }
60
+
61
+ if (vin === undefined) {
62
+ return cbk([400, 'ExpectedUtxoSpendIndexToSignReplacementTx']);
63
+ }
64
+
65
+ return cbk();
66
+ },
67
+
68
+ // Find the local key index for the channel
69
+ findKeyIndex: ['validate', ({}, cbk) => {
70
+ const funding = hexAsBuffer(script);
71
+
72
+ const keys = decompile(funding).filter(isBuffer).map(bufferAsHex);
73
+
74
+ return asyncDetectLimit(range(multiKeyLimit), limit, (index, cbk) => {
75
+ return getPublicKey({family, index, lnd}, (err, res) => {
76
+ if (!!err) {
77
+ return cbk(err);
78
+ }
79
+
80
+ return cbk(null, keys.includes(res.public_key));
81
+ });
82
+ },
83
+ cbk);
84
+ }],
85
+
86
+ // Get the public key
87
+ getKey: ['findKeyIndex', ({findKeyIndex}, cbk) => {
88
+ if (findKeyIndex === undefined) {
89
+ return cbk([503, 'FailedToFindChannelMultiSigKeyIndex']);
90
+ }
91
+
92
+ return getPublicKey({family, lnd, index: findKeyIndex}, cbk);
93
+ }],
94
+
95
+ // Sign the channel funds spend
96
+ signSpend: ['findKeyIndex', ({findKeyIndex}, cbk) => {
97
+ const inputs = [{
98
+ vin,
99
+ key_family: family,
100
+ key_index: findKeyIndex,
101
+ output_script: output,
102
+ output_tokens: capacity,
103
+ sighash: SIGHASH_ALL,
104
+ witness_script: script,
105
+ }];
106
+
107
+ return signTransaction({inputs, lnd, transaction}, cbk);
108
+ }],
109
+
110
+ // Final signature
111
+ signature: ['getKey', 'signSpend', ({getKey, signSpend}, cbk) => {
112
+ const [signature] = signSpend.signatures;
113
+
114
+ return cbk(null, {signature, key: getKey.public_key});
115
+ }],
116
+ },
117
+ returnResult({reject, resolve, of: 'signature'}, cbk));
118
+ });
119
+ };