paid-services 3.2.0 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/capacity/accept_capacity_change.js +284 -0
- package/capacity/ask_for_change_details.js +505 -0
- package/capacity/ask_for_decrease.js +185 -0
- package/capacity/assemble_replacement_tx.js +171 -0
- package/capacity/capacity_change_requests.js +76 -0
- package/capacity/change_channel_capacity.js +183 -0
- package/capacity/encode_change_request.js +55 -0
- package/capacity/encode_sign_capacity_response.js +32 -0
- package/capacity/fee_for_replacement.js +108 -0
- package/capacity/finalize_capacity_replacement.js +67 -0
- package/capacity/get_capacity_change_requests.js +98 -0
- package/capacity/get_capacity_replacement.js +371 -0
- package/capacity/get_replacement_signature.js +232 -0
- package/capacity/index.js +4 -0
- package/capacity/initiate_capacity_change.js +506 -0
- package/capacity/interim_replacement_psbt.js +57 -0
- package/capacity/is_open_witness_script.js +40 -0
- package/capacity/parse_accept_request.js +58 -0
- package/capacity/parse_accept_response.js +52 -0
- package/capacity/parse_capacity_change_request.js +101 -0
- package/capacity/parse_sign_capacity_request.js +59 -0
- package/capacity/propose_capacity_change.js +361 -0
- package/capacity/service_types.json +6 -0
- package/capacity/sign_capacity_replacement.js +119 -0
- package/capacity/witness_script_from_close_txs.js +50 -0
- package/index.js +2 -0
- package/p2p/encode_peer_response.js +0 -1
- package/p2p/make_peer_request.js +3 -3
- package/package.json +6 -4
- package/test/capacity/test_assemble_replacement_tx.js +46 -0
- package/test/capacity/test_capacity_change_requests.js +54 -0
- package/test/capacity/test_encode_change_request.js +49 -0
- package/test/capacity/test_encode_sign_capacity_response.js +47 -0
- package/test/capacity/test_fee_for_replacement.js +40 -0
- package/test/capacity/test_finalize_capacity_replacement.js +45 -0
- package/test/capacity/test_interim_replacement_psbt.js +44 -0
- package/test/capacity/test_is_open_witness_script.js +40 -0
- package/test/capacity/test_parse_accept_request.js +42 -0
- package/test/capacity/test_parse_capacity_change_request.js +51 -0
- package/test/capacity/test_parse_sign_capacity_request.js +49 -0
- package/test/capacity/test_witness_script_from_close_txs.js +53 -0
- package/test/integration/test_accept_capacity_change.js +152 -0
- package/test/integration/test_change_channel_capacity.js +196 -0
- package/test/integration/test_decrease_channel_capacity.js +206 -0
- package/test/integration/test_increase_channel_capacity.js +197 -0
- package/test/integration/test_sign_capacity_replacement.js +142 -0
- package/test/p2p/test_encode_peer_request.js +53 -0
- package/test/p2p/test_encode_peer_response.js +38 -0
- package/test/p2p/test_encode_response_code.js +48 -0
- package/trades/network_from_network_record.js +6 -0
- package/trades/network_record_from_request.js +5 -0
|
@@ -0,0 +1,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
|
+
};
|