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,506 @@
1
+ const {randomBytes} = require('crypto');
2
+
3
+ const asyncAuto = require('async/auto');
4
+ const asyncReflect = require('async/reflect');
5
+ const asyncUntil = require('async/until');
6
+ const {acceptsChannelOpen} = require('ln-sync');
7
+ const {broadcastChainTransaction} = require('ln-service');
8
+ const {broadcastTransaction} = require('ln-sync');
9
+ const {closeChannel} = require('ln-service');
10
+ const {getChainFeeRate} = require('ln-service');
11
+ const {getChainTransactions} = require('ln-service');
12
+ const {getChannels} = require('ln-service');
13
+ const {getFeeRates} = require('ln-service');
14
+ const {getNetwork} = require('ln-sync');
15
+ const {payments} = require('bitcoinjs-lib');
16
+ const {reserveTransitFunds} = require('ln-sync');
17
+ const {returnResult} = require('asyncjs-util');
18
+ const {stopAllHtlcs} = require('ln-sync');
19
+ const {Transaction} = require('bitcoinjs-lib');
20
+
21
+ const askForChangeDetails = require('./ask_for_change_details');
22
+ const {makePeerRequest} = require('./../p2p');
23
+ const parseAcceptRequest = require('./parse_accept_request');
24
+ const proposeCapacityChange = require('./propose_capacity_change');
25
+ const {servicePeerRequests} = require('./../p2p');
26
+ const {serviceTypeAcceptCapacityChange} = require('./service_types');
27
+ const {serviceTypeChangeCapacity} = require('./service_types');
28
+ const {serviceTypeSignCapacityChange} = require('./service_types');
29
+ const {serviceTypeWaitOnConfirmation} = require('./service_types');
30
+
31
+ const bufferAsHex = buffer => buffer.toString('hex');
32
+ const capacityChangeRequestIdType = '0';
33
+ const customMessageType = 50800;
34
+ const family = 0;
35
+ const familyTemporary = 805;
36
+ const {fromHex} = Transaction;
37
+ const hexAsBuffer = hex => Buffer.from(hex, 'hex');
38
+ const idRecordType = '0';
39
+ const makeId = () => randomBytes(32).toString('hex');
40
+ const messageFrequencyMs = 3000;
41
+ const minimumAddTokens = 20000;
42
+ const multiKeyLimit = 100000;
43
+ const openTxType = '1';
44
+ const {p2wpkh} = payments;
45
+ const range = max => [...Array(max).keys()];
46
+ const rebroadcastTimeMs = 1000 * 5;
47
+ const recordsAsObject = a => a.reduce((sum, n) => sum[n.type] = n.value, {});
48
+ const requestCapacityChangeIntervalMs = 2000;
49
+ const requestCapacityChangeTimeoutMs = 1000 * 60 * 60 * 6;
50
+ const sumOf = arr => arr.reduce((sum, n) => sum + n, Number());
51
+ const testMessage = '00';
52
+ const tokensAsBigUnit = tokens => (tokens / 1e8).toFixed(8);
53
+ const uniq = arr => Array.from(new Set(arr));
54
+ const waitingPingDelayMs = 1000 * 5;
55
+ const waitingTimeoutMs = 1000 * 30;
56
+
57
+ /** Initiate a channel capacity change
58
+
59
+ {
60
+ ask: <Ask Function>
61
+ lnd: <Authenticated LND API Object>
62
+ logger: <Winston Logger Object>
63
+ }
64
+
65
+ @returns via cbk or Promise
66
+ {
67
+ base_fee_mtokens: <Forwarding Base Fee Millitokens String>
68
+ cltv_delta: <Forwarding CLTV Delta Number>
69
+ fee_rate: <Forwarding Parts Per Million Fee Rate Number>
70
+ transaction_id: <Replacement Channel Transaction Id Hex String>
71
+ transaction_vout: <Replacement Channel Transaction Output Index Number>
72
+ }
73
+ */
74
+ module.exports = ({ask, lnd, logger}, cbk) => {
75
+ return new Promise((resolve, reject) => {
76
+ return asyncAuto({
77
+ // Generate a unique identifier for the change
78
+ id: cbk => cbk(null, makeId()),
79
+
80
+ // Check arguments
81
+ validate: cbk => {
82
+ if (!ask) {
83
+ return cbk([400, 'ExpectedAskFunctionToInitiateCapacityChange']);
84
+ }
85
+
86
+ if (!lnd) {
87
+ return cbk([400, 'ExpectedLndToInitiateCapacityChange']);
88
+ }
89
+
90
+ if (!logger) {
91
+ return cbk([400, 'ExpectedLoggerObjectToInitiateCapacityChange']);
92
+ }
93
+
94
+ return cbk();
95
+ },
96
+
97
+ // Get the network name
98
+ getNetwork: ['validate', ({}, cbk) => getNetwork({lnd}, cbk)],
99
+
100
+ // Ask for all the capacity change details
101
+ askForChangeDetails: ['id', 'validate', ({id}, cbk) => {
102
+ return askForChangeDetails({ask, id, lnd}, cbk);
103
+ }],
104
+
105
+ // Propose the theoretical new channel to the peer to confirm a change
106
+ checkAccept: ['askForChangeDetails', ({askForChangeDetails}, cbk) => {
107
+ // Check that a channel open would be accepted
108
+ return acceptsChannelOpen({
109
+ lnd,
110
+ capacity: askForChangeDetails.estimated_capacity,
111
+ cooperative_close_address: askForChangeDetails.coop_close_address,
112
+ give_tokens: askForChangeDetails.remote_balance,
113
+ is_private: askForChangeDetails.is_private,
114
+ min_htlc_mtokens: askForChangeDetails.min_htlc_mtokens,
115
+ partner_csv_delay: askForChangeDetails.partner_csv_delay,
116
+ partner_public_key: askForChangeDetails.partner_public_key,
117
+ },
118
+ cbk);
119
+ }],
120
+
121
+ // Get the additional capacity funding
122
+ getFunding: [
123
+ 'askForChangeDetails',
124
+ 'checkAccept',
125
+ ({askForChangeDetails}, cbk) =>
126
+ {
127
+ // Exit early when additional funding is not necessary
128
+ if (!askForChangeDetails.increase) {
129
+ return cbk(null, {});
130
+ }
131
+
132
+ const tokens = askForChangeDetails.increase;
133
+
134
+ // Get a signed tx ready that pays the increase in funds
135
+ return reserveTransitFunds({ask, lnd, logger, tokens}, cbk);
136
+ }],
137
+
138
+ // Wait for the basic capacity change proposal to be acknowledged
139
+ sendBasicRequest: [
140
+ 'askForChangeDetails',
141
+ 'getFunding',
142
+ 'id',
143
+ ({askForChangeDetails, getFunding, id}, cbk) =>
144
+ {
145
+ const records = [];
146
+ const response = {};
147
+ const service = servicePeerRequests({lnd});
148
+ const start = new Date();
149
+ const type = serviceTypeAcceptCapacityChange;
150
+
151
+ if (!!getFunding.refund) {
152
+ logger.info({add_funds_signed_refund_tx: getFunding.refund});
153
+ }
154
+
155
+ if (!!askForChangeDetails.open_transaction) {
156
+ records.push({
157
+ type: openTxType,
158
+ value: askForChangeDetails.open_transaction,
159
+ });
160
+ }
161
+
162
+ // Listen for the acceptance of the capacity change
163
+ service.request({type}, (req, res) => {
164
+ // Exit early when the request is not from the peer
165
+ if (req.from !== askForChangeDetails.partner_public_key) {
166
+ return;
167
+ }
168
+
169
+ const findIdRecord = records => records.find(n => n.type === '0');
170
+
171
+ const idRecord = findIdRecord(req.records);
172
+
173
+ // Exit early when the request is not for this change request
174
+ if (!idRecord || idRecord.value !== id) {
175
+ return;
176
+ }
177
+
178
+ logger.info({peer_accepted_request: true});
179
+
180
+ // Stop waiting for a response now that the request has been ack'ed
181
+ service.stop({});
182
+
183
+ res.success({records});
184
+
185
+ const {transaction} = parseAcceptRequest({
186
+ records: req.records,
187
+ open_transaction: askForChangeDetails.open_transaction,
188
+ transaction_id: askForChangeDetails.transaction_id,
189
+ });
190
+
191
+ if (!transaction) {
192
+ const error = [400, 'FundingTxRequiredToChangeCapacity'];
193
+
194
+ response.error = error;
195
+
196
+ return res.failure(error);
197
+ }
198
+
199
+ response.transaction = transaction;
200
+
201
+ return;
202
+ });
203
+
204
+ logger.info({waiting_for_peer_acceptance: true});
205
+
206
+ // This is a repeating request, the peer might not be online
207
+ return asyncUntil(
208
+ cbk => {
209
+ if (!!response.error || !!response.transaction) {
210
+ return cbk(null, true);
211
+ }
212
+
213
+ if (new Date() - start > requestCapacityChangeTimeoutMs) {
214
+ return cbk([408, 'TimedOutWaitingForChangeAcceptance']);
215
+ }
216
+
217
+ // Stop new pings when there is an ack of the change request
218
+ return cbk(null, false);
219
+ },
220
+ cbk => {
221
+ return makePeerRequest({
222
+ lnd,
223
+ records: askForChangeDetails.records,
224
+ timeout: requestCapacityChangeIntervalMs,
225
+ to: askForChangeDetails.partner_public_key,
226
+ type: serviceTypeChangeCapacity,
227
+ },
228
+ () => {
229
+ // Continue pinging with the proposal
230
+ return setTimeout(cbk, requestCapacityChangeIntervalMs);
231
+ });
232
+ },
233
+ err => {
234
+ service.stop({});
235
+
236
+ if (!!response.error) {
237
+ return cbk(response.error);
238
+ }
239
+
240
+ if (!!err) {
241
+ return cbk(err);
242
+ }
243
+
244
+ return cbk(null, response.transaction);
245
+ }
246
+ );
247
+ }],
248
+
249
+ // Get the current channels to double check there is enough local balance
250
+ getPeerChannels: [
251
+ 'askForChangeDetails',
252
+ 'sendBasicRequest',
253
+ ({askForChangeDetails}, cbk) =>
254
+ {
255
+ return getChannels({
256
+ lnd,
257
+ partner_public_key: askForChangeDetails.partner_public_key,
258
+ },
259
+ cbk);
260
+ }],
261
+
262
+ // Stop all HTLCs with the peer until the channel is closed
263
+ stopHtlcs: [
264
+ 'askForChangeDetails',
265
+ 'getPeerChannels',
266
+ ({askForChangeDetails, getPeerChannels}, cbk) =>
267
+ {
268
+ return stopAllHtlcs({
269
+ lnd,
270
+ id: askForChangeDetails.id,
271
+ ids: getPeerChannels.channels.map(n => n.id),
272
+ peer: askForChangeDetails.partner_public_key,
273
+ },
274
+ cbk);
275
+ }],
276
+
277
+ // Broadcast and confirm the add funds transaction into a block
278
+ confirmAddFunds: [
279
+ 'askForChangeDetails',
280
+ 'getFunding',
281
+ 'getPeerChannels',
282
+ 'id',
283
+ 'sendBasicRequest',
284
+ asyncReflect(({
285
+ askForChangeDetails,
286
+ getFunding,
287
+ getPeerChannels,
288
+ id,
289
+ sendBasicRequest,
290
+ },
291
+ cbk) =>
292
+ {
293
+ const channel = getPeerChannels.channels.find(({id}) => {
294
+ return id === askForChangeDetails.id;
295
+ });
296
+
297
+ if (!channel) {
298
+ return cbk([400, 'FailedToFindCapacityChangeChannel']);
299
+ }
300
+
301
+ const localBalance = channel.local_balance;
302
+
303
+ if (!!channel.pending_payments.length) {
304
+ return cbk([400, 'ExpectedNoPendingPaymentsToChangeCapacity']);
305
+ }
306
+
307
+ // Make sure that we still have enough balance in local balance
308
+ if (localBalance + askForChangeDetails.estimated_local_delta < 0) {
309
+ return cbk([400, 'InsufficientRemainingFundsToChangeCapacity']);
310
+ }
311
+
312
+ let isHeartbeatAcked = false;
313
+ let isReady = false;
314
+
315
+ // This will take some time so start pinging the peer that we are here
316
+ asyncUntil(
317
+ cbk => cbk(null, isReady),
318
+ cbk => {
319
+ return makePeerRequest({
320
+ lnd,
321
+ records: [{type: capacityChangeRequestIdType, value: id}],
322
+ timeout: waitingTimeoutMs,
323
+ to: askForChangeDetails.partner_public_key,
324
+ type: serviceTypeWaitOnConfirmation,
325
+ },
326
+ err => {
327
+ // The peer needs to respond to waiting heartbeats
328
+ if (!!err) {
329
+ return cbk(err);
330
+ }
331
+
332
+ isHeartbeatAcked = true;
333
+
334
+ return setTimeout(cbk, waitingPingDelayMs);
335
+ });
336
+ },
337
+ cbk
338
+ );
339
+
340
+ // Exit early when there is no funding transaction to broadcast
341
+ if (!getFunding.transaction) {
342
+ isReady = true;
343
+
344
+ return;
345
+ }
346
+
347
+ const channelId = askForChangeDetails.id;
348
+
349
+ // Get the funding confirmed
350
+ return asyncUntil(
351
+ cbk => {
352
+ return getChainTransactions({lnd}, (err, res) => {
353
+ if (!!err) {
354
+ return cbk(err);
355
+ }
356
+
357
+ const tx = res.transactions
358
+ .filter(tx => tx.is_confirmed)
359
+ .find(tx => tx.transaction === getFunding.transaction);
360
+
361
+ // Exit early when there is no confirmed transaction
362
+ if (!tx) {
363
+ logger.info({waiting_for_funds_confirmation: getFunding.id});
364
+
365
+ return cbk(null, false);
366
+ }
367
+
368
+ // Transaction is confirmed in a block
369
+ logger.info({funds_confirmed: true});
370
+
371
+ return cbk(null, true);
372
+ });
373
+ },
374
+ cbk => {
375
+ return broadcastChainTransaction({
376
+ lnd,
377
+ description: `Funds for increasing capacity on ${channelId}`,
378
+ transaction: getFunding.transaction,
379
+ },
380
+ err => {
381
+ if (!!err) {
382
+ logger.error({err});
383
+ }
384
+
385
+ return setTimeout(cbk, rebroadcastTimeMs);
386
+ });
387
+ },
388
+ () => {
389
+ // Stop the "we are here" pinging
390
+ return isReady = true;
391
+ }
392
+ );
393
+ })],
394
+
395
+ // Propose the capacity change to the peer
396
+ proposeChange: [
397
+ 'askForChangeDetails',
398
+ 'confirmAddFunds',
399
+ 'getFunding',
400
+ 'getNetwork',
401
+ 'getPeerChannels',
402
+ 'id',
403
+ 'sendBasicRequest',
404
+ asyncReflect(({
405
+ askForChangeDetails,
406
+ getFunding,
407
+ getNetwork,
408
+ getPeerChannels,
409
+ id,
410
+ sendBasicRequest,
411
+ },
412
+ cbk) =>
413
+ {
414
+ const channel = getPeerChannels.channels.find(channel => {
415
+ return channel.id === askForChangeDetails.id
416
+ });
417
+
418
+ logger.info({
419
+ change_channel: {
420
+ id: askForChangeDetails.id,
421
+ transaction_id: channel.transaction_id,
422
+ transaction_vout: channel.transaction_vout,
423
+ },
424
+ });
425
+
426
+ return proposeCapacityChange({
427
+ id,
428
+ lnd,
429
+ logger,
430
+ bitcoinjs_network: getNetwork.bitcoinjs,
431
+ channel: askForChangeDetails.id,
432
+ decrease: askForChangeDetails.decrease,
433
+ increase: askForChangeDetails.increase,
434
+ increase_key: getFunding.key,
435
+ increase_key_index: getFunding.index,
436
+ increase_output_script: getFunding.output,
437
+ increase_transaction: getFunding.transaction,
438
+ increase_transaction_id: getFunding.id,
439
+ increase_transaction_vout: getFunding.vout,
440
+ increase_witness_script: getFunding.script,
441
+ open_transaction: sendBasicRequest,
442
+ partner_public_key: askForChangeDetails.partner_public_key,
443
+ transaction_id: channel.transaction_id,
444
+ transaction_vout: channel.transaction_vout,
445
+ },
446
+ cbk);
447
+ })],
448
+
449
+ // The replacement channel details
450
+ replacementChannel: [
451
+ 'askForChangeDetails',
452
+ 'proposeChange',
453
+ ({askForChangeDetails, proposeChange}, cbk) =>
454
+ {
455
+ // Exit early with no replacement when the proposal fails
456
+ if (!!proposeChange.error) {
457
+ return cbk();
458
+ }
459
+
460
+ logger.info({published_replacement_channel: proposeChange.value});
461
+
462
+ return cbk(null, {
463
+ base_fee_mtokens: askForChangeDetails.base_fee_mtokens,
464
+ cltv_delta: askForChangeDetails.cltv_delta,
465
+ fee_rate: askForChangeDetails.fee_rate,
466
+ transaction_id: proposeChange.value.transaction_id,
467
+ transaction_vout: proposeChange.value.transaction_vout,
468
+ });
469
+ }],
470
+
471
+ // Broadcast the add capacity refund transaction if there was an error
472
+ executeRefund: [
473
+ 'confirmAddFunds',
474
+ 'getFunding',
475
+ 'proposeChange',
476
+ ({confirmAddFunds, getFunding, proposeChange}, cbk) =>
477
+ {
478
+ // Exit early when there were no funds added
479
+ if (!confirmAddFunds) {
480
+ return cbk();
481
+ }
482
+
483
+ // Exit early when there is no need to refund
484
+ if (!confirmAddFunds.error && !proposeChange.error) {
485
+ return cbk();
486
+ }
487
+
488
+ const capacityChangeErr = confirmAddFunds.error || proposeChange.error;
489
+
490
+ return broadcastChainTransaction({
491
+ lnd,
492
+ transaction: getFunding.refund,
493
+ description: `Refund for failed capacity change`,
494
+ },
495
+ err => {
496
+ if (!!err) {
497
+ logger.error({err});
498
+ }
499
+
500
+ return cbk(capacityChangeErr);
501
+ });
502
+ }],
503
+ },
504
+ returnResult({reject, resolve, of: 'replacementChannel'}, cbk));
505
+ });
506
+ };
@@ -0,0 +1,57 @@
1
+ const {Transaction} = require('bitcoinjs-lib');
2
+ const {transactionAsPsbt} = require('psbt');
3
+
4
+ const hexAsBuffer = hex => Buffer.from(hex, 'hex');
5
+ const {fromHex} = Transaction;
6
+
7
+ /** Generate a dummy PSBT to allow for setting up the channel funding
8
+
9
+ {
10
+ [increase_transaction]: <Increase Funds Transaction Hex String>
11
+ open_transaction: <Original Channel Funding Transaction Hex String>
12
+ signature: <Hex Encoded Signature String>
13
+ unsigned_transaction: <Unsigned Capacity Change Replacement Tx Hex String>
14
+ witness_script: <Hex Encoded Funding Witness Script String>
15
+ }
16
+
17
+ @returns
18
+ {
19
+ psbt: <Dummy PSBT For Funding Pending Channel Hex String>
20
+ }
21
+ */
22
+ module.exports = args => {
23
+ const fundingScript = Buffer.from(args.witness_script, 'hex');
24
+ const replacement = fromHex(args.unsigned_transaction);
25
+ const spending = [args.increase_transaction, args.open_transaction];
26
+
27
+ const signature = Buffer.concat([
28
+ hexAsBuffer(args.signature),
29
+ Buffer.from([Transaction.SIGHASH_ALL]),
30
+ ]);
31
+
32
+ const closeSpendWitnessStack = [signature, signature, fundingScript];
33
+
34
+ replacement.ins.forEach((n, i) => {
35
+ return replacement.setWitness(i, closeSpendWitnessStack);
36
+ });
37
+
38
+ // An additional signature is required when adding funds
39
+ if (!!args.increase_transaction) {
40
+ const addFundsSignature = Buffer.concat([
41
+ hexAsBuffer(args.increase_signature),
42
+ Buffer.from([Transaction.SIGHASH_ALL]),
43
+ ]);
44
+
45
+ replacement.setWitness(args.increase_transaction_vin, [
46
+ addFundsSignature,
47
+ hexAsBuffer(args.increase_public_key),
48
+ ]);
49
+ }
50
+
51
+ const {psbt} = transactionAsPsbt({
52
+ spending: spending.filter(n => !!n),
53
+ transaction: replacement.toHex(),
54
+ });
55
+
56
+ return {psbt};
57
+ };
@@ -0,0 +1,40 @@
1
+ const {script} = require('bitcoinjs-lib');
2
+
3
+ const {decompile} = script;
4
+ const expectedScriptElements = 5;
5
+ const hexAsBuffer = hex => Buffer.from(hex, 'hex');
6
+ const isPublicKey = n => (n[0] === 2 || n[0] === 3) && n.length === 33;
7
+ const OP_2 = 82;
8
+ const OP_CHECKMULTISIG = 174;
9
+
10
+ /** Determine if a witness script looks like a channel open
11
+
12
+ {
13
+ script: <Hex Encoded Witness Script>
14
+ }
15
+
16
+ @returns
17
+ <Looks Like Open Witness Script Bool>
18
+ */
19
+ module.exports = ({script}) => {
20
+ const elements = decompile(hexAsBuffer(script));
21
+
22
+ if (elements.length !== expectedScriptElements) {
23
+ return false;
24
+ }
25
+
26
+ const [requiredSigs, key1, key2, numKeys, checkMultiSig] = elements;
27
+
28
+ // A channel funding output is a 2:2
29
+ if (requiredSigs !== OP_2 || numKeys !== OP_2) {
30
+ return false;
31
+ }
32
+
33
+ // There are 2 public keys in the script
34
+ if (!isPublicKey(key1) || !isPublicKey(key2)) {
35
+ return false;
36
+ }
37
+
38
+ // The OP code checks that the sigs both agree on the spend
39
+ return checkMultiSig === OP_CHECKMULTISIG;
40
+ };
@@ -0,0 +1,58 @@
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 request
7
+
8
+ {
9
+ open_transaction: <Channel Funding Transaction Hex String>
10
+ records: [{
11
+ type: <Record Type Number String>
12
+ value: <Record Value Hex Encoded String>
13
+ }]
14
+ transaction_id: <Channel to Capacity Change Transaction Id Hex String>
15
+ }
16
+
17
+ @throws
18
+ <Error>
19
+
20
+ @returns
21
+ {
22
+ transaction: <Channel Open Transaction Hex String>
23
+ }
24
+ */
25
+ module.exports = args => {
26
+ // Exit early when there are no records
27
+ if (!args.records) {
28
+ return {};
29
+ }
30
+
31
+ // Exit early when there is a local open transaction
32
+ if (!!args.open_transaction) {
33
+ return {transaction: args.open_transaction};
34
+ }
35
+
36
+ const txRecord = findTxRecord(args.records);
37
+
38
+ // Exit early when there is no tx record
39
+ if (!txRecord) {
40
+ return {};
41
+ }
42
+
43
+ // Check to make sure there is a TX record
44
+ try {
45
+ fromHex(txRecord.value);
46
+ } catch (err) {
47
+ throw new Error('ExpectedValidRawTransactionHexToParseAcceptResponse');
48
+ }
49
+
50
+ const txId = fromHex(txRecord.value).getId();
51
+
52
+ // Check to make sure the TX record matches the tx id
53
+ if (txId !== args.transaction_id) {
54
+ throw new Error('ExpectedTxRecordForRawChannelTransaction');
55
+ }
56
+
57
+ return {transaction: txRecord.value};
58
+ };