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,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,101 @@
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 idHexLength = 64;
8
+ const tooLarge = BigInt(Number.MAX_SAFE_INTEGER);
9
+
10
+ const findChannelRecord = records => records.find(n => n.type === '2');
11
+ const findDecreaseRecord = records => records.find(n => n.type === '3');
12
+ const findIncreaseRecord = records => records.find(n => n.type === '4');
13
+ const findRequestIdRecord = records => records.find(n => n.type === '1');
14
+ const findVersionRecord = records => records.find(n => n.type === '0');
15
+
16
+ /** Parse a capacity change request
17
+
18
+ {
19
+ from: <From Public Key Hex String>
20
+ [records]: [{
21
+ type: <Record Type Number String>
22
+ value: <Record Hex Encoded Value String>
23
+ }]
24
+ }
25
+
26
+ @returns
27
+ {
28
+ [request]: {
29
+ channel: <Standard Format Channel Id String>
30
+ [decrease]: <Reduce Capacity Tokens Number>
31
+ from: <From Node Public Key Id Hex String>
32
+ id: <Change Request Id Hex String>
33
+ [increase]: <Add Capacity Tokens Number>
34
+ }
35
+ }
36
+ */
37
+ module.exports = ({from, records}) => {
38
+ // Exit early when there are no records
39
+ if (!records) {
40
+ return {};
41
+ }
42
+
43
+ // Exit early when there is a version record, indicating a future version
44
+ if (!!findVersionRecord(records)) {
45
+ return {};
46
+ }
47
+
48
+ const channelRecord = findChannelRecord(records);
49
+
50
+ // Exit early when there is no channel
51
+ if (!channelRecord || channelRecord.value.length !== channelIdHexLength) {
52
+ return {};
53
+ }
54
+
55
+ try {
56
+ chanFormat({id: channelRecord.value});
57
+ } catch (err) {
58
+ return {};
59
+ }
60
+
61
+ const idRecord = findRequestIdRecord(records);
62
+
63
+ // Exit early when there is no request id
64
+ if (!idRecord || idRecord.value.length !== idHexLength) {
65
+ return {};
66
+ }
67
+
68
+ const decreaseRecord = findDecreaseRecord(records);
69
+ const increaseRecord = findIncreaseRecord(records);
70
+
71
+ // Exit early when there is a change in both directions
72
+ if (!!decreaseRecord && !!increaseRecord) {
73
+ return {};
74
+ }
75
+
76
+ const {channel} = chanFormat({id: channelRecord.value});
77
+ const id = idRecord.value;
78
+
79
+ // Exit early when there is no decrease or increase
80
+ if (!decreaseRecord && !increaseRecord) {
81
+ return {request: {channel, from, id}};
82
+ }
83
+
84
+ const decrease = decodeNumber((decreaseRecord || defaultRecord).value);
85
+ const increase = decodeNumber((increaseRecord || defaultRecord).value);
86
+
87
+ // Exit early when there is no reasonably sized increase or decrease
88
+ if (decrease > tooLarge || increase > tooLarge) {
89
+ return {};
90
+ }
91
+
92
+ return {
93
+ request: {
94
+ channel,
95
+ from,
96
+ id,
97
+ decrease: Number(decrease) || undefined,
98
+ increase: Number(increase) || undefined,
99
+ },
100
+ };
101
+ };
@@ -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,361 @@
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
+ [increase]: <Increase Channel Size Tokens Number>
44
+ [increase_key_index]: <Increase Funds Transit Key Number>
45
+ [increase_output_script]: <Increase Funds Output Script Hex String>
46
+ [increase_transaction_id]: <Increase Funds Outpoint Tx Id Hex String>
47
+ [increase_transaction_vout]: <Increase Funds Outpoint Output Index Number>
48
+ [increase_witness_script]: <Increase Funds Witness Script Hex String>
49
+ lnd: <Authenticated LND API Object>
50
+ logger: <Winston Logger Object>
51
+ open_transaction: <Original Channel Funding Transaction Hex String>
52
+ partner_public_key: <Peer Public Key Hex Encoded String>
53
+ transaction_id: <Original Channel Transaction Id Hex String>
54
+ transaction_vout: <Original Channel Transaction Output Index Number>
55
+ }
56
+
57
+ @returns via cbk or Promise
58
+ {
59
+ transaction_id: <Replacement Channel Transaction Id Hex String>
60
+ transaction_vout: <Replacement Channel Transaction Output Index Number>
61
+ }
62
+ */
63
+ module.exports = (args, cbk) => {
64
+ return new Promise((resolve, reject) => {
65
+ return asyncAuto({
66
+ // Check arguments
67
+ validate: cbk => {
68
+ if (!args.bitcoinjs_network) {
69
+ return cbk([400, 'ExpectedBitcoinjsNetworkNameToProposeChange']);
70
+ }
71
+
72
+ if (!args.channel) {
73
+ return cbk([400, 'ExpectedChannelIdToProposeCapacityChangeFor']);
74
+ }
75
+
76
+ if (!args.decrease) {
77
+ return cbk([400, 'ExpectedDecreaseSetToProposeCapacityChange']);
78
+ }
79
+
80
+ if (!args.id) {
81
+ return cbk([400, 'ExpectedChangeRequestIdToProposeCapacityChange']);
82
+ }
83
+
84
+ if (!args.lnd) {
85
+ return cbk([400, 'ExpectedAuthenticatedLndToProposeCapacityChange']);
86
+ }
87
+
88
+ if (!args.logger) {
89
+ return cbk([400, 'ExpectedWinstonLoggerToProposeCapacityChange']);
90
+ }
91
+
92
+ if (!args.open_transaction) {
93
+ return cbk([400, 'ExpectedChannelOpenTxToProposeCapacityChange']);
94
+ }
95
+
96
+ if (!args.partner_public_key) {
97
+ return cbk([400, 'ExpectedPeerNodeIdToProposeCapacityChange']);
98
+ }
99
+
100
+ if (!args.transaction_id) {
101
+ return cbk([400, 'ExpectedOriginalTxIdToProposeCapacityChange']);
102
+ }
103
+
104
+ if (args.transaction_vout === undefined) {
105
+ return cbk([400, 'ExpectedOriginalChannelTxOutputIndexToPropose']);
106
+ }
107
+
108
+ return cbk();
109
+ },
110
+
111
+ // Get the starting height
112
+ getHeight: ['validate', ({}, cbk) => getHeight({lnd: args.lnd}, cbk)],
113
+
114
+ // Close the current channal and get the unsigned replacement
115
+ getReplacement: ['validate', ({}, cbk) => {
116
+ return getCapacityReplacement({
117
+ add_funds_transaction_id: args.increase_transaction_id,
118
+ add_funds_transaction_vout: args.increase_transaction_vout,
119
+ bitcoinjs_network: args.bitcoinjs_network,
120
+ decrease: args.decrease,
121
+ id: args.channel,
122
+ increase: args.increase,
123
+ lnd: args.lnd,
124
+ open_transaction: args.open_transaction,
125
+ transaction_id: args.transaction_id,
126
+ transaction_vout: args.transaction_vout,
127
+ },
128
+ cbk);
129
+ }],
130
+
131
+ // Replacement channel outpoint
132
+ replacement: ['getReplacement', ({getReplacement}, cbk) => {
133
+ args.logger.info({pending_chan_id: getReplacement.pending_channel_id});
134
+
135
+ return cbk(null, {
136
+ transaction_id: getReplacement.transaction_id,
137
+ transaction_vout: getReplacement.transaction_vout,
138
+ });
139
+ }],
140
+
141
+ // Partially sign the replacement transaction using the add funds key
142
+ signAddFunds: ['getReplacement', ({getReplacement}, cbk) => {
143
+ // Exit early when there are no additional funds added
144
+ if (!args.increase) {
145
+ return cbk(null, {});
146
+ }
147
+
148
+ return signTransaction({
149
+ inputs: [{
150
+ key_family: transitFamily,
151
+ key_index: args.increase_key_index,
152
+ output_script: args.increase_output_script,
153
+ output_tokens: args.increase,
154
+ sighash: SIGHASH_ALL,
155
+ vin: getReplacement.add_funds_vin,
156
+ witness_script: args.increase_witness_script,
157
+ }],
158
+ lnd: args.lnd,
159
+ transaction: getReplacement.unsigned_transaction,
160
+ },
161
+ cbk);
162
+ }],
163
+
164
+ // Use the unsigned replacement transaction to "fund" the pending channel
165
+ fundWithReplacement: [
166
+ 'getReplacement',
167
+ 'signAddFunds',
168
+ ({getReplacement, signAddFunds}, cbk) =>
169
+ {
170
+ const [addFundsSignature] = signAddFunds.signatures || [];
171
+
172
+ const {psbt} = interimReplacementPsbt({
173
+ increase_public_key: args.increase_key,
174
+ increase_signature: addFundsSignature,
175
+ increase_transaction: args.increase_transaction,
176
+ increase_transaction_vin: getReplacement.add_funds_vin,
177
+ open_transaction: args.open_transaction,
178
+ signature: getReplacement.signature,
179
+ unsigned_transaction: getReplacement.unsigned_transaction,
180
+ witness_script: getReplacement.witness_script,
181
+ });
182
+
183
+ // Create the commitment transaction with the peer on the replacement
184
+ return fundPendingChannels({
185
+ channels: [getReplacement.pending_channel_id],
186
+ funding: psbt,
187
+ lnd: args.lnd,
188
+ },
189
+ cbk);
190
+ }],
191
+
192
+ // Send the final go ahead signal now that a proposal is made
193
+ getPeerSignature: [
194
+ 'getReplacement',
195
+ 'fundWithReplacement',
196
+ asyncReflect(({getReplacement}, cbk) =>
197
+ {
198
+ return makePeerRequest({
199
+ lnd: args.lnd,
200
+ records: [
201
+ {
202
+ type: unsignedTransactionType,
203
+ value: getReplacement.unsigned_transaction,
204
+ },
205
+ {
206
+ type: capacityChangeIdType,
207
+ value: args.id,
208
+ },
209
+ ],
210
+ timeout: peerRequestTimeoutMs,
211
+ to: args.partner_public_key,
212
+ type: serviceTypeSignCapacityChange,
213
+ },
214
+ (err, res) => {
215
+ if (!!err) {
216
+ return cbk(err);
217
+ }
218
+
219
+ if (!res.records) {
220
+ return cbk([503, 'ExpectedSignatureResponseFromPeerForChange']);
221
+ }
222
+
223
+ const record = findSignatureRecord(res.records);
224
+
225
+ if (!record || record.value.length > maxSigHexLength) {
226
+ return cbk([503, 'ExpectedSignatureRecordFromPeerForChange']);
227
+ }
228
+
229
+ return cbk(null, record.value);
230
+ });
231
+ })],
232
+
233
+ // Cancel the funding proposal when there is an error
234
+ cancelFunding: [
235
+ 'getPeerSignature',
236
+ 'getReplacement',
237
+ ({getPeerSignature, getReplacement}, cbk) =>
238
+ {
239
+ // Exit early when there is no need to cancel
240
+ if (!getPeerSignature.error) {
241
+ return cbk();
242
+ }
243
+
244
+ return cancelPendingChannel({
245
+ id: getReplacement.pending_channel_id,
246
+ lnd: args.lnd,
247
+ },
248
+ err => {
249
+ if (!!err) {
250
+ return cbk([503, 'FailedToCancelPendingChannel', {err}]);
251
+ }
252
+
253
+ // Return the original error
254
+ return cbk(getPeerSignature.error);
255
+ });
256
+ }],
257
+
258
+ // Broadcast the new funding transaction using the peer's signature
259
+ confirmFunding: [
260
+ 'getHeight',
261
+ 'getPeerSignature',
262
+ 'getReplacement',
263
+ 'signAddFunds',
264
+ ({getHeight, getPeerSignature, getReplacement, signAddFunds}, cbk) =>
265
+ {
266
+ // Exit early when there was an error getting the peer signature
267
+ if (!!getPeerSignature.error) {
268
+ return cbk();
269
+ }
270
+
271
+ const [addFundsSignature] = signAddFunds.signatures || [];
272
+
273
+ // Insert the peer signature to get the final signed funding tx
274
+ const {transaction} = finalizeCapacityReplacement({
275
+ add_funds_vin: getReplacement.add_funds_vin,
276
+ add_funds_public_key: args.increase_key,
277
+ add_funds_signature: addFundsSignature,
278
+ local_public_key: getReplacement.signing_key,
279
+ local_signature: getReplacement.signature,
280
+ funding_spend_vin: getReplacement.transaction_vin,
281
+ remote_signature: getPeerSignature.value,
282
+ transaction: getReplacement.unsigned_transaction,
283
+ witness_script: getReplacement.witness_script,
284
+ });
285
+
286
+ args.logger.info({publishing_new_channel_transaction: transaction});
287
+
288
+ return asyncDoUntil(
289
+ cbk => {
290
+ // Try and override the force close tx using the replacement
291
+ return broadcastChainTransaction({
292
+ transaction,
293
+ after: getHeight.current_block_height - fuzzHeight,
294
+ lnd: args.lnd,
295
+ },
296
+ cbk);
297
+ },
298
+ (broadcast, cbk) => {
299
+ args.logger.info({waiting_for_confirmation: broadcast.id});
300
+
301
+ // Rebroadcast until the transaction is confirmed
302
+ return getChainTransactions({lnd: args.lnd}, (err, res) => {
303
+ if (!!err) {
304
+ return cbk(err);
305
+ }
306
+
307
+ const confirmed = res.transactions.filter(n => !!n.is_confirmed);
308
+
309
+ // Find the confirmed tx
310
+ const tx = confirmed.find(tx => tx.id === broadcast.id);
311
+
312
+ if (!!tx) {
313
+ return cbk(null, true);
314
+ }
315
+
316
+ // Find the replacing tx
317
+ const replacing = confirmed.find(tx => {
318
+ return tx.id === getReplacement.force_close_tx_id;
319
+ });
320
+
321
+ // Cancel the pending channel if the force close confirms
322
+ if (!!replacing) {
323
+ return cancelPendingChannel({
324
+ id: getReplacement.pending_channel_id,
325
+ lnd: args.lnd,
326
+ },
327
+ () => {
328
+ return cbk([503, 'FailedToReplaceForceCloseTransaction']);
329
+ });
330
+ }
331
+
332
+ return getChannels({
333
+ lnd: args.lnd,
334
+ partner_public_key: args.partner_public_key,
335
+ },
336
+ (err, res) => {
337
+ if (!!err) {
338
+ return cbk(err);
339
+ }
340
+
341
+ const channel = res.channels.find(channel => {
342
+ return channel.transaction_id === broadcast.id
343
+ });
344
+
345
+ // Stop broadcasting when a channel shows up
346
+ if (!!channel) {
347
+ return cbk(null, true);
348
+ }
349
+
350
+ // Rebroadcast after a delay
351
+ return setTimeout(() => cbk(null, false), rebroadcastDelayMs);
352
+ });
353
+ });
354
+ },
355
+ cbk
356
+ );
357
+ }],
358
+ },
359
+ returnResult({reject, resolve, of: 'replacement'}, cbk));
360
+ });
361
+ };
@@ -0,0 +1,6 @@
1
+ {
2
+ "serviceTypeAcceptCapacityChange": "8050002",
3
+ "serviceTypeChangeCapacity": "8050001",
4
+ "serviceTypeSignCapacityChange": "805004",
5
+ "serviceTypeWaitOnConfirmation": "8050003"
6
+ }
@@ -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
+ };