paid-services 3.17.2 → 3.19.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Versions
2
2
 
3
+ ## Version 3.19.1
4
+
5
+ - `manageSwap`: Add support for inbound peer constraint
6
+ - `manageSwap`: Add support for external sweep address
7
+
8
+ ## Version 3.18.0
9
+
10
+ - `manageGroupJoin`: Add conflict tx for safe funded pending channel deletion
11
+
3
12
  ## Version 3.17.2
4
13
 
5
14
  - `manageGroupJoin`: Add method to coordinate or join a channels group
@@ -1,4 +1,5 @@
1
1
  const asyncAuto = require('async/auto');
2
+ const asyncRetry = require('async/retry');
2
3
  const {connectPeer} = require('ln-sync');
3
4
  const {getChainBalance} = require('ln-service');
4
5
  const {getNodeAlias} = require('ln-sync');
@@ -8,9 +9,11 @@ const {getGroupDetails} = require('./p2p');
8
9
 
9
10
  const coordinatorFromJoinCode = n => n.slice(0, 66);
10
11
  const groupIdFromJoinCode = n => n.slice(66);
12
+ const interval = 500;
11
13
  const isCode = n => !!n && n.length === 98;
12
14
  const isPublicKey = n => !!n && /^0[2-3][0-9A-F]{64}$/i.test(n);
13
15
  const niceName = n => `${n.alias} ${n.id}`.trim();
16
+ const times = 2 * 60;
14
17
  const tokensAsBigUnit = tokens => (tokens / 1e8).toFixed(8);
15
18
 
16
19
  /** Ask to confirm joining a group
@@ -73,7 +76,10 @@ module.exports = ({ask, lnd}, cbk) => {
73
76
 
74
77
  // Connect to the coordinator
75
78
  connect: ['group', ({group}, cbk) => {
76
- return connectPeer({lnd, id: group.coordinator}, cbk);
79
+ return asyncRetry({interval, times}, cbk => {
80
+ return connectPeer({lnd, id: group.coordinator}, cbk);
81
+ },
82
+ cbk);
77
83
  }],
78
84
 
79
85
  // Get the coordinator node alias
@@ -78,6 +78,9 @@ module.exports = ({capacity, count, ecp, identity, lnd, rate}) => {
78
78
  return emitter.emit('error', err);
79
79
  };
80
80
 
81
+ // An error was encountered
82
+ coordinator.events.once('error', errored);
83
+
81
84
  // Group members have registered themselves
82
85
  coordinator.events.once('joined', async ({ids}) => {
83
86
  emitter.emit('filled', {ids});
@@ -70,7 +70,9 @@ module.exports = ({ask, lnd, logger}, cbk) => {
70
70
  return logger.info({peering_with: formatNodes(nodes)});
71
71
  });
72
72
 
73
- join.once('publishing', ({signed}) => logger.info({signed}));
73
+ join.once('publishing', ({refund, signed}) => {
74
+ return logger.info({refund, signed});
75
+ });
74
76
 
75
77
  return;
76
78
  }],
@@ -13,6 +13,8 @@ const half = n => n / 2;
13
13
  lnd: <Authenticated LND API Object>
14
14
  to: <Look for Outgoing Channel To Identity Public Key Hex String>
15
15
  }
16
+
17
+ @returns via cbk or Promise
16
18
  */
17
19
  module.exports = ({capacity, from, id, lnd, to}, cbk) => {
18
20
  return new Promise((resolve, reject) => {
@@ -1,18 +1,28 @@
1
+ const {address} = require('bitcoinjs-lib');
1
2
  const asyncAuto = require('async/auto');
2
3
  const asyncRetry = require('async/retry');
4
+ const {createChainAddress} = require('ln-service');
5
+ const {createPsbt} = require('psbt');
3
6
  const {decodePsbt} = require('psbt');
4
7
  const {extendPsbt} = require('psbt');
5
8
  const {fundPendingChannels} = require('ln-service');
9
+ const {getChainFeeRate} = require('ln-service');
10
+ const {getMaxFundAmount} = require('ln-sync');
6
11
  const {getPendingChannels} = require('ln-service');
7
12
  const {partiallySignPsbt} = require('ln-service');
13
+ const {payments} = require('bitcoinjs-lib');
8
14
  const {returnResult} = require('asyncjs-util');
15
+ const {signPsbt} = require('ln-service');
9
16
  const tinysecp = require('tiny-secp256k1');
10
17
  const {Transaction} = require('bitcoinjs-lib');
11
18
  const {unextractTransaction} = require('psbt');
12
19
 
20
+ const bufferAsHex = buffer => buffer.toString('hex');
13
21
  const {concat} = Buffer;
14
22
  const dummySignature = Buffer.alloc(1);
23
+ const format = 'p2wpkh';
15
24
  const {from} = Buffer;
25
+ const {fromBech32} = address;
16
26
  const {fromHex} = Transaction;
17
27
  const hashAll = Transaction.SIGHASH_ALL;
18
28
  const hashDefault = Transaction.SIGHASH_DEFAULT;
@@ -21,6 +31,9 @@ const inputAsOutpoint = n => `${n.transaction_id}:${n.transaction_vout}`;
21
31
  const interval = 10;
22
32
  const {isArray} = Array;
23
33
  const notEmpty = arr => arr.filter(n => !!n);
34
+ const {p2wpkh} = payments;
35
+ const {random} = Math;
36
+ const slowConf = 144;
24
37
  const spendAsOutpoint = n => `${n.hash.reverse().toString('hex')}:${n.index}`;
25
38
  const times = 500;
26
39
 
@@ -49,6 +62,7 @@ const times = 500;
49
62
 
50
63
  @returns via cbk or Promise
51
64
  {
65
+ conflict: <Conflict Transaction Hex String>
52
66
  psbt: <Partially Signed PSBT Hex String>
53
67
  }
54
68
  */
@@ -79,11 +93,94 @@ module.exports = ({id, lnd, psbt, utxos}, cbk) => {
79
93
  return cbk();
80
94
  },
81
95
 
96
+ // Create a conflicting address to refund funds to
97
+ createConflictAddress: ['validate', ({}, cbk) => {
98
+ return createChainAddress({format, lnd}, cbk);
99
+ }],
100
+
101
+ // Get the conflicting tx fee rate
102
+ getRate: ['validate', ({}, cbk) => {
103
+ return getChainFeeRate({lnd, confirmation_target: slowConf}, cbk);
104
+ }],
105
+
106
+ // Find the conflicting amount to send to the refund address
107
+ getConflictAmount: [
108
+ 'createConflictAddress',
109
+ 'getRate',
110
+ ({createConflictAddress, getRate}, cbk) =>
111
+ {
112
+ const [input] = utxos;
113
+
114
+ return getMaxFundAmount({
115
+ lnd,
116
+ addresses: [createConflictAddress.address],
117
+ fee_tokens_per_vbyte: getRate.tokens_per_vbyte,
118
+ inputs: [{
119
+ tokens: input.witness_utxo.tokens,
120
+ transaction_id: input.transaction_id,
121
+ transaction_vout: input.transaction_vout,
122
+ }],
123
+ },
124
+ cbk);
125
+ }],
126
+
127
+ // Create a conflicting PSBT to sign
128
+ conflict: [
129
+ 'createConflictAddress',
130
+ 'ecp',
131
+ 'getConflictAmount',
132
+ ({createConflictAddress, ecp, getConflictAmount}, cbk) =>
133
+ {
134
+ const hash = fromBech32(createConflictAddress.address).data;
135
+ const [input] = utxos;
136
+
137
+ const {psbt} = createPsbt({
138
+ outputs: [{
139
+ script: bufferAsHex(p2wpkh({hash}).output),
140
+ tokens: getConflictAmount.max_tokens,
141
+ }],
142
+ utxos: [{
143
+ id: input.transaction_id,
144
+ vout: input.transaction_vout,
145
+ }],
146
+ });
147
+
148
+ const base = decodePsbt({ecp, psbt});
149
+
150
+ const tx = fromHex(base.unsigned_transaction);
151
+
152
+ const inputs = base.inputs.map((input, vin) => {
153
+ const outpoint = spendAsOutpoint(tx.ins[vin]);
154
+
155
+ // Look for relevant signing instructions
156
+ const utxo = utxos.find(n => inputAsOutpoint(n) === outpoint) || {};
157
+
158
+ return {
159
+ bip32_derivations: utxo.bip32_derivations,
160
+ non_witness_utxo: utxo.non_witness_utxo,
161
+ sighash_type: !!utxo.non_witness_utxo ? hashAll : hashDefault,
162
+ witness_utxo: utxo.witness_utxo,
163
+ };
164
+ });
165
+
166
+ // Extend the base PSBT with relevant signing information
167
+ return cbk(null, extendPsbt({ecp, inputs, psbt}).psbt);
168
+ }],
169
+
170
+ // Decode the PSBT to get the unsigned funding transaction
171
+ funding: ['ecp', 'validate', ({ecp}, cbk) => {
172
+ try {
173
+ return cbk(null, decodePsbt({ecp, psbt}));
174
+ } catch (err) {
175
+ return cbk([400, 'ExpectedValidPsbtToSignAndFundChannel', {err}]);
176
+ }
177
+ }],
178
+
82
179
  // Extend the PSBT with the derivation paths
83
- psbtToSign: ['ecp', 'validate', ({ecp}, cbk) => {
84
- const tx = fromHex(decodePsbt({ecp, psbt}).unsigned_transaction);
180
+ psbtToSign: ['ecp', 'funding', ({ecp, funding}, cbk) => {
181
+ const tx = fromHex(funding.unsigned_transaction);
85
182
 
86
- const inputs = decodePsbt({ecp, psbt}).inputs.map((input, vin) => {
183
+ const inputs = funding.inputs.map((input, vin) => {
87
184
  const outpoint = spendAsOutpoint(tx.ins[vin]);
88
185
 
89
186
  // Look for relevant signing instructions
@@ -101,6 +198,11 @@ module.exports = ({id, lnd, psbt, utxos}, cbk) => {
101
198
  return cbk(null, {psbt: extendPsbt({ecp, inputs, psbt}).psbt});
102
199
  }],
103
200
 
201
+ // Sign and finalize the conflicting PSBT
202
+ signConflict: ['conflict', ({conflict}, cbk) => {
203
+ return signPsbt({lnd, psbt: conflict}, cbk);
204
+ }],
205
+
104
206
  // Partially sign the PSBT that funds the channel open
105
207
  signPsbt: ['psbtToSign', ({psbtToSign}, cbk) => {
106
208
  return partiallySignPsbt({lnd, psbt: psbtToSign.psbt}, cbk);
@@ -151,7 +253,7 @@ module.exports = ({id, lnd, psbt, utxos}, cbk) => {
151
253
  }],
152
254
 
153
255
  // Fund the pending channel with the finalized PSBT
154
- fundChannel: ['finalizePsbt', ({finalizePsbt}, cbk) => {
256
+ fundChannel: ['conflict', 'finalizePsbt', ({finalizePsbt}, cbk) => {
155
257
  return fundPendingChannels({
156
258
  lnd,
157
259
  channels: [id],
@@ -182,7 +284,35 @@ module.exports = ({id, lnd, psbt, utxos}, cbk) => {
182
284
  },
183
285
  cbk);
184
286
  }],
287
+
288
+ // Final group funding transaction resolution
289
+ result: [
290
+ 'ecp',
291
+ 'signConflict',
292
+ 'signPsbt',
293
+ ({ecp, signConflict, signPsbt}, cbk) =>
294
+ {
295
+ const signed = decodePsbt({ecp, psbt: signPsbt.psbt});
296
+
297
+ const extended = extendPsbt({
298
+ ecp,
299
+ psbt,
300
+ inputs: signed.inputs.map(input => {
301
+ return {
302
+ non_witness_utxo: input.non_witness_utxo,
303
+ partial_sig: input.partial_sig,
304
+ taproot_key_spend_sig: input.taproot_key_spend_sig,
305
+ witness_utxo: input.witness_utxo,
306
+ };
307
+ }),
308
+ });
309
+
310
+ return cbk(null, {
311
+ conflict: signConflict.transaction,
312
+ psbt: extended.psbt,
313
+ });
314
+ }],
185
315
  },
186
- returnResult({reject, resolve, of: 'signPsbt'}, cbk));
316
+ returnResult({reject, resolve, of: 'result'}, cbk));
187
317
  });
188
318
  };
@@ -1,8 +1,11 @@
1
1
  const EventEmitter = require('events');
2
2
 
3
3
  const asyncAuto = require('async/auto');
4
+ const asyncReflect = require('async/reflect');
4
5
  const asyncRetry = require('async/retry');
6
+ const {cancelPendingChannel} = require('ln-service');
5
7
  const {decodePsbt} = require('psbt');
8
+ const {deletePendingChannel} = require('ln-service');
6
9
  const {returnResult} = require('asyncjs-util');
7
10
  const tinysecp = require('tiny-secp256k1');
8
11
  const {Transaction} = require('bitcoinjs-lib');
@@ -16,6 +19,7 @@ const {registerPendingOpen} = require('./p2p');
16
19
  const {registerSignedOpen} = require('./p2p');
17
20
 
18
21
  const {fromHex} = Transaction;
22
+ const hexAsBuffer = hex => Buffer.from(hex, 'hex');
19
23
  const interval = 1000;
20
24
  const times = 60 * 10;
21
25
 
@@ -138,10 +142,22 @@ module.exports = ({capacity, coordinator, count, id, lnd, rate}, cbk) => {
138
142
  }],
139
143
 
140
144
  // Decode the unsigned PSBT
141
- transaction: ['ecp', 'register', ({ecp, register}, cbk) => {
145
+ transaction: [
146
+ 'ecp',
147
+ 'propose',
148
+ 'register',
149
+ ({ecp, propose, register}, cbk) =>
150
+ {
151
+ const funding = hexAsBuffer(propose.funding);
142
152
  const psbt = decodePsbt({ecp, psbt: register.psbt});
143
153
 
144
- return cbk(null, {id: fromHex(psbt.unsigned_transaction).getId()});
154
+ const tx = fromHex(psbt.unsigned_transaction);
155
+
156
+ return cbk(null, {
157
+ id: tx.getId(),
158
+ raw: psbt.unsigned_transaction,
159
+ vout: tx.outs.findIndex(n => n.script.equals(funding)),
160
+ });
145
161
  }],
146
162
 
147
163
  // Confirm the incoming channel
@@ -150,7 +166,7 @@ module.exports = ({capacity, coordinator, count, id, lnd, rate}, cbk) => {
150
166
  'partners',
151
167
  'register',
152
168
  'transaction',
153
- ({ecp, partners, register, transaction}, cbk) =>
169
+ asyncReflect(({ecp, partners, register, transaction}, cbk) =>
154
170
  {
155
171
  // Make sure that there is an inbound channel
156
172
  return asyncRetry({interval, times}, cbk => {
@@ -164,12 +180,45 @@ module.exports = ({capacity, coordinator, count, id, lnd, rate}, cbk) => {
164
180
  cbk);
165
181
  },
166
182
  cbk);
183
+ })],
184
+
185
+ // Clean up funding pending channels when the channel group fails
186
+ clean: [
187
+ 'incoming',
188
+ 'propose',
189
+ 'register',
190
+ 'transaction',
191
+ ({incoming, propose, register, transaction}, cbk) =>
192
+ {
193
+ // Exit early when incoming channel is seen
194
+ if (!incoming.error) {
195
+ return cbk();
196
+ }
197
+
198
+ // When there was no incoming channel detected, fail and clean up
199
+ return deletePendingChannel({
200
+ lnd,
201
+ confirmed_transaction: register.conflict,
202
+ pending_transaction: transaction.raw,
203
+ pending_transaction_vout: transaction.vout,
204
+ },
205
+ err => {
206
+ if (!!err) {
207
+ return cbk([503, 'UnexpectedErrorCleaningUpGroupChannel', {err}]);
208
+ }
209
+
210
+ // Pass back the original error
211
+ return cbk(incoming.error);
212
+ });
167
213
  }],
168
214
 
169
215
  // Publish partial signatures to coordinator
170
- reveal: ['incoming', 'register', ({register}, cbk) => {
216
+ reveal: ['clean', 'incoming', 'register', ({register}, cbk) => {
171
217
  // Let listeners know that the signature will be sent to coordinator
172
- emitter.emit('publishing', {signed: register.psbt});
218
+ emitter.emit('publishing', {
219
+ refund: register.conflict,
220
+ signed: register.psbt,
221
+ });
173
222
 
174
223
  return registerSignedOpen({
175
224
  coordinator,
@@ -1,5 +1,7 @@
1
1
  const asyncAuto = require('async/auto');
2
+ const asyncReflect = require('async/reflect');
2
3
  const asyncRetry = require('async/retry');
4
+ const {cancelPendingChannel} = require('ln-service');
3
5
  const {connectPeer} = require('ln-sync');
4
6
  const {decodePsbt} = require('psbt');
5
7
  const {returnResult} = require('asyncjs-util');
@@ -46,7 +48,8 @@ const typeGroupChannelId = '1';
46
48
 
47
49
  @returns via cbk or Promise
48
50
  {
49
- psbt: <Unsigned PSBT Hex String>
51
+ conflict: <Conflict Transaction Hex String>
52
+ psbt: <Partially Signed PSBT Hex String>
50
53
  }
51
54
  */
52
55
  module.exports = (args, cbk) => {
@@ -94,7 +97,7 @@ module.exports = (args, cbk) => {
94
97
  }],
95
98
 
96
99
  // Send connection confirmation request
97
- request: ['connect', ({}, cbk) => {
100
+ request: ['connect', asyncReflect(({}, cbk) => {
98
101
  const {records} = encodePendingProposal({
99
102
  change: args.change,
100
103
  funding: args.funding,
@@ -145,17 +148,34 @@ module.exports = (args, cbk) => {
145
148
  });
146
149
  },
147
150
  cbk);
151
+ })],
152
+
153
+ // Clean up the pending channel if registration fails
154
+ clean: ['request', ({request}, cbk) => {
155
+ // Exit early when there was no error registering the pending channel
156
+ if (!request.error) {
157
+ return cbk();
158
+ }
159
+
160
+ return cancelPendingChannel({id: args.pending, lnd: args.lnd}, err => {
161
+ if (!!err) {
162
+ return cbk([503, 'UnexpectedErrorCleaningPendingChannel', {err}]);
163
+ }
164
+
165
+ // Return the original registration error
166
+ return cbk(request.error);
167
+ });
148
168
  }],
149
169
 
150
170
  // Check the unsigned funding transaction represents the partial open
151
- check: ['ecp', 'request', ({ecp, request}, cbk) => {
171
+ check: ['clean', 'ecp', 'request', ({ecp, request}, cbk) => {
152
172
  try {
153
- decodePsbt({ecp, psbt: request});
173
+ decodePsbt({ecp, psbt: request.value});
154
174
  } catch (err) {
155
175
  return cbk([503, 'ExpectedValidUnsignedResponsePsbt', {err}]);
156
176
  }
157
177
 
158
- const psbt = decodePsbt({ecp, psbt: request});
178
+ const psbt = decodePsbt({ecp, psbt: request.value});
159
179
 
160
180
  const tx = fromHex(psbt.unsigned_transaction);
161
181
 
@@ -207,7 +227,7 @@ module.exports = (args, cbk) => {
207
227
  return signAndFundGroupChannel({
208
228
  id: args.pending,
209
229
  lnd: args.lnd,
210
- psbt: request,
230
+ psbt: request.value,
211
231
  utxos: args.utxos,
212
232
  },
213
233
  cbk);
package/package.json CHANGED
@@ -15,10 +15,10 @@
15
15
  "bolt07": "1.8.2",
16
16
  "ecpair": "2.0.1",
17
17
  "goldengate": "11.2.3",
18
- "invoices": "2.0.7",
18
+ "invoices": "2.1.0",
19
19
  "ln-service": "53.17.4",
20
20
  "ln-sync": "3.13.0",
21
- "psbt": "2.6.0",
21
+ "psbt": "2.7.0",
22
22
  "p2tr": "1.3.1",
23
23
  "tiny-secp256k1": "2.2.1"
24
24
  },
@@ -47,5 +47,5 @@
47
47
  "integration-tests": "tap -j 2 --branches=1 --functions=1 --lines=1 --statements=1 -t 180 test/integration/*.js",
48
48
  "test": "tap --branches=1 --functions=1 --lines=1 --statements=1 -t 60 test/actions/*.js test/balanced/*.js test/capacity/*.js test/client/*.js test/config/*.js test/p2p/*.js test/records/*.js test/respond/*.js test/server/*.js test/server/*.js test/services/*.js test/trades/*.js"
49
49
  },
50
- "version": "3.17.2"
50
+ "version": "3.19.1"
51
51
  }
@@ -90,6 +90,7 @@ const v1AddressWords = key => [].concat(1).concat(bech32m.toWords(key));
90
90
  recovery: <Swap Request Recovery Hex String>
91
91
  [request]: <Request Function>
92
92
  response: <Swap Response Hex String>
93
+ [sweep_address]: <Sweep Chain Address String>
93
94
  }
94
95
 
95
96
  @returns via cbk or Promise
@@ -134,6 +135,11 @@ module.exports = (args, cbk) => {
134
135
 
135
136
  // Create a sweep address
136
137
  createAddress: ['validate', ({}, cbk) => {
138
+ // Exit early when there is no need to create a sweep address
139
+ if (!!args.sweep_address) {
140
+ return cbk(null, {address: args.sweep_address});
141
+ }
142
+
137
143
  return createChainAddress({lnd: args.lnd}, cbk);
138
144
  }],
139
145
 
@@ -204,6 +210,7 @@ module.exports = (args, cbk) => {
204
210
  coop_public_key: decoded.coop_public_key,
205
211
  deposit_mtokens: decoded.deposit_mtokens,
206
212
  deposit_payment: decoded.deposit_payment,
213
+ incoming_peer: decoded.incoming_peer,
207
214
  push: decoded.push,
208
215
  refund_public_key: decoded.refund_public_key,
209
216
  request: decoded.request,
@@ -238,7 +245,10 @@ module.exports = (args, cbk) => {
238
245
  const request = responseDetails.request;
239
246
 
240
247
  if (!!args.is_external_funding) {
241
- args.emitter.emit('update', {pay_to_fund_swap_offchain: request});
248
+ args.emitter.emit('update', {
249
+ external_funding_pay_to_fund_swap_offchain: request,
250
+ must_be_in_through: responseDetails.incoming_peer || undefined,
251
+ });
242
252
 
243
253
  return cbk();
244
254
  }
@@ -247,6 +257,7 @@ module.exports = (args, cbk) => {
247
257
 
248
258
  return pay({
249
259
  request,
260
+ incoming_peer: responseDetails.incoming_peer || undefined,
250
261
  lnd: args.lnd,
251
262
  max_fee: args.max_fee_funding,
252
263
  },
@@ -381,6 +392,7 @@ module.exports = (args, cbk) => {
381
392
  cltv_delta: to.cltv_delta,
382
393
  destination: to.destination,
383
394
  features: to.features,
395
+ incoming_peer: requestDetails.incoming_peer || undefined,
384
396
  lnd: args.lnd,
385
397
  max_fee: args.max_fee_deposit,
386
398
  messages: [{
@@ -412,7 +424,10 @@ module.exports = (args, cbk) => {
412
424
 
413
425
  const address = encodeAddress(prefix, v1AddressWords(key));
414
426
 
415
- args.emitter.emit('update', {waiting_for_chain_funding: address});
427
+ args.emitter.emit('update', {
428
+ waiting_for_chain_funding: address,
429
+ required_confirmations: args.min_confirmations || defaultConfsCount,
430
+ });
416
431
 
417
432
  if (!!args.request) {
418
433
  return findDeposit({
@@ -14,6 +14,7 @@ const {diffieHellmanComputeSecret} = require('ln-service');
14
14
  const {fundPsbt} = require('ln-service');
15
15
  const {getChainFeeRate} = require('ln-service');
16
16
  const {getChainTransactions} = require('ln-service');
17
+ const {getChannels} = require('ln-service');
17
18
  const {getHeight} = require('ln-service');
18
19
  const {getIdentity} = require('ln-service');
19
20
  const {getInvoice} = require('ln-service');
@@ -159,6 +160,7 @@ module.exports = (args, cbk) => {
159
160
  claim_solo_public_key: details.claim_solo_public_key,
160
161
  hash: details.hash,
161
162
  key_index: details.key_index,
163
+ incoming_peer: details.incoming_peer || undefined,
162
164
  refund_coop_private_key: details.refund_coop_private_key,
163
165
  refund_coop_private_key_hash: details.refund_coop_private_key_hash,
164
166
  solo_private_key: details.refund_solo_private_key,
@@ -170,6 +172,19 @@ module.exports = (args, cbk) => {
170
172
  }
171
173
  }],
172
174
 
175
+ // Lookup channel ids with the incoming peer when applicable
176
+ getChannels: ['recoveryDetails', ({recoveryDetails}, cbk) => {
177
+ if (!recoveryDetails.incoming_peer) {
178
+ return cbk();
179
+ }
180
+
181
+ return getChannels({
182
+ lnd: args.lnd,
183
+ partner_public_key: recoveryDetails.incoming_peer,
184
+ },
185
+ cbk);
186
+ }],
187
+
173
188
  // Get the refund key
174
189
  getRefundKey: [
175
190
  'ecp',
@@ -213,6 +228,7 @@ module.exports = (args, cbk) => {
213
228
  return;
214
229
  }
215
230
 
231
+ const channels = invoice.payments.map(n => n.in_channel);
216
232
  const hash = recoveryDetails.claim_coop_public_key_hash;
217
233
  const timeout = min(...invoice.payments.map(n => n.timeout));
218
234
 
@@ -231,7 +247,11 @@ module.exports = (args, cbk) => {
231
247
 
232
248
  args.emitter.emit('update', {execution_payment_held: id});
233
249
 
234
- return cbk(null, {delta, claim_coop_public_key: message.value});
250
+ return cbk(null, {
251
+ channels,
252
+ delta,
253
+ claim_coop_public_key: message.value,
254
+ });
235
255
  });
236
256
  }],
237
257
 
@@ -257,13 +277,14 @@ module.exports = (args, cbk) => {
257
277
 
258
278
  args.emitter.emit('update', {offchain_funding_held: id});
259
279
 
280
+ const channels = invoice.payments.map(n => n.in_channel);
260
281
  const timeout = min(...invoice.payments.map(n => n.timeout));
261
282
 
262
283
  const delta = timeout - recoveryDetails.timeout;
263
284
 
264
285
  sub.removeAllListeners();
265
286
 
266
- return cbk(null, {delta});
287
+ return cbk(null, {channels, delta});
267
288
  });
268
289
 
269
290
  return;
@@ -423,12 +444,52 @@ module.exports = (args, cbk) => {
423
444
  cbk);
424
445
  }],
425
446
 
447
+ // Check incoming peer constraints
448
+ checkIncomingPeer: [
449
+ 'getChannels',
450
+ 'waitForDepositHold',
451
+ 'waitForFundHold',
452
+ ({getChannels, waitForDepositHold, waitForFundHold}, cbk) =>
453
+ {
454
+ // Exit early when there are no incoming peer constraints
455
+ if (!args.incoming_peer) {
456
+ return cbk();
457
+ }
458
+
459
+ const channels = getChannels.channels.map(n => n.id);
460
+
461
+ // The deposit must come in under a channel with the incoming peer
462
+ const invalidDeposit = waitForDepositHold.channels.find(id => {
463
+ return !channels.includes(id);
464
+ });
465
+
466
+ if (!!invalidDeposit) {
467
+ return cbk([503, 'UnexpectedInboundChannelForHeldDeposit']);
468
+ }
469
+
470
+ // The funding must come in under a channel with the incoming peer
471
+ const invalidFunding = waitForFundHold.channels.find(id => {
472
+ return !channels.includes(id);
473
+ });
474
+
475
+ if (!!invalidFunding) {
476
+ return cbk([503, 'UnexpectedInboundChannelForHeldFunding']);
477
+ }
478
+
479
+ return cbk();
480
+ }],
481
+
426
482
  // Check that there are sufficient remaining blocks
427
483
  checkTimeRemaining: [
428
484
  'recoveryDetails',
429
485
  'waitForDepositHold',
430
486
  'waitForFundHold',
431
- ({recoveryDetails, waitForDepositHold, waitForFundHold}, cbk) =>
487
+ ({
488
+ recoveryDetails,
489
+ waitForDepositHold,
490
+ waitForFundHold,
491
+ },
492
+ cbk) =>
432
493
  {
433
494
  if (waitForDepositHold.delta < defaultMinDelta) {
434
495
  return cbk([503, 'InsufficientDepositDeltaBlocksToFundSwap']);
@@ -458,6 +519,7 @@ module.exports = (args, cbk) => {
458
519
 
459
520
  // Lock chain funds to fund the swap with
460
521
  lockFunding: [
522
+ 'checkIncomingPeer',
461
523
  'checkTimeRemaining',
462
524
  'getTransactions',
463
525
  'swap',
@@ -3,6 +3,7 @@ const {decodeTlvStream} = require('bolt01');
3
3
 
4
4
  const {requestRecordsAsRequest} = require('./../records');
5
5
  const {publicTypes} = require('./swap_field_types');
6
+ const {swapVersion} = require('./swap_field_types');
6
7
 
7
8
  const decodeNumber = encoded => decodeBigSize({encoded}).decoded;
8
9
  const findRecord = (records, type) => records.find(n => n.type === type);
@@ -13,6 +14,7 @@ const paymentNonceLength = 64;
13
14
  const startIndex = 0;
14
15
  const typeCoopPrivateKeyHash = publicTypes.typeRefundCoopPrivateKeyHash;
15
16
  const {typeDeposit} = publicTypes;
17
+ const {typeInboundPeer} = publicTypes;
16
18
  const {typePush} = publicTypes;
17
19
  const {typeRefundCoopPublicKey} = publicTypes;
18
20
  const {typeRefundSoloPublicKey} = publicTypes;
@@ -33,6 +35,7 @@ const {typeVersion} = publicTypes;
33
35
  coop_public_key: <Refund Cooperative Key Hex String>
34
36
  deposit_mtokens: <Deposit Amount Millitokens Number String>
35
37
  deposit_payment: <Deposit Payment Nonce Hex String>
38
+ [incoming_peer]: <Constrained to Inbound Peer Public Key Id Hex String>
36
39
  push: <Push Payment Nonce Hex String>
37
40
  refund_public_key: <Refund Unilateral Public Key Hex String>
38
41
  request: <BOLT 11 Encoded Funding Request String>
@@ -56,10 +59,22 @@ module.exports = ({network, response}) => {
56
59
 
57
60
  const {records} = decodeTlvStream({encoded: response});
58
61
 
59
- if (!!findRecord(records, typeVersion)) {
62
+ if (!findRecord(records, typeVersion)) {
60
63
  throw new Error('UnexpectedVersionOfOffToOnResponse');
61
64
  }
62
65
 
66
+ const versionRecord = findRecord(records, typeVersion);
67
+
68
+ try {
69
+ decodeNumber(versionRecord.value);
70
+ } catch (err) {
71
+ throw new Error('ExpectedValidVersionRecordForOffToOnResponse');
72
+ }
73
+
74
+ if (Number(decodeNumber(versionRecord.value)) !== swapVersion) {
75
+ throw new Error('UnsupportedSwapVersionNumberForOffchainToOnchainSwap');
76
+ }
77
+
63
78
  const coopPrivateKeyHashRecord = findRecord(records, typeCoopPrivateKeyHash);
64
79
 
65
80
  if (!coopPrivateKeyHashRecord) {
@@ -106,6 +121,12 @@ module.exports = ({network, response}) => {
106
121
  throw new Error('ExpectedSmallerDepositAmountInOffToOnResponse');
107
122
  }
108
123
 
124
+ const inboundRecord = findRecord(records, typeInboundPeer);
125
+
126
+ if (!!inboundRecord && !isPublicKey(inboundRecord.value)) {
127
+ throw new Error('ExpectectedPublicKeyForInboundPeerConstraint');
128
+ }
129
+
109
130
  const pushRecord = findRecord(records, typePush);
110
131
 
111
132
  if (!pushRecord) {
@@ -150,6 +171,7 @@ module.exports = ({network, response}) => {
150
171
  coop_public_key: coopPublicKeyRecord.value,
151
172
  deposit_mtokens: decodeBigSize({encoded: depositAmount}).decoded,
152
173
  deposit_payment: depositRecord.value.slice(startIndex, paymentNonceLength),
174
+ incoming_peer: !!inboundRecord ? inboundRecord.value : undefined,
153
175
  push: pushRecord.value,
154
176
  refund_public_key: soloPublicKeyRecord.value,
155
177
  request: funding.request,
@@ -5,6 +5,7 @@ const {decodeTlvStream} = require('bolt01');
5
5
 
6
6
  const decodeSwapSecrets = require('./decode_swap_secrets');
7
7
  const {publicTypes} = require('./swap_field_types');
8
+ const {swapVersion} = require('./swap_field_types');
8
9
 
9
10
  const decodeNumber = encoded => decodeBigSize({encoded}).decoded;
10
11
  const findRecord = (records, type) => records.find(n => n.type === type);
@@ -16,6 +17,7 @@ const sha256 = preimage => createHash('sha256').update(preimage).digest('hex');
16
17
  const {typeClaimCoopPublicKeyHash} = publicTypes;
17
18
  const {typeClaimSoloPublicKey} = publicTypes;
18
19
  const {typeHash} = publicTypes;
20
+ const {typeInboundPeer} = publicTypes;
19
21
  const {typePrivateRefundDetails} = publicTypes;
20
22
  const {typeRefundCoopPrivateKeyHash} = publicTypes;
21
23
  const {typeTimeout} = publicTypes;
@@ -60,8 +62,17 @@ module.exports = ({decrypt, recovery}) => {
60
62
 
61
63
  const {records} = decodeTlvStream({encoded: recovery});
62
64
 
63
- if (!!findRecord(records, typeVersion)) {
64
- throw new Error('UnexpectedVersionOfOffToOnRecovery');
65
+ // Make sure the recovery records are for the known swap version
66
+ const versionRecord = findRecord(records, typeVersion);
67
+
68
+ try {
69
+ decodeNumber(versionRecord.value);
70
+ } catch (err) {
71
+ throw new Error('ExpectedValidVersionRecordForOffToOnRecovery');
72
+ }
73
+
74
+ if (Number(decodeNumber(versionRecord.value)) !== swapVersion) {
75
+ throw new Error('UnsupportedSwapVersionNumberForOffToOnRecovery');
65
76
  }
66
77
 
67
78
  const claimCoopPubKeyHash = findRecord(records, typeClaimCoopPublicKeyHash);
@@ -74,6 +85,12 @@ module.exports = ({decrypt, recovery}) => {
74
85
  throw new Error('ExpectedValidClaimCoopPublicKeyHash');
75
86
  }
76
87
 
88
+ const inboundRecord = findRecord(records, typeInboundPeer);
89
+
90
+ if (!!inboundRecord && !isPublicKey(inboundRecord.value)) {
91
+ throw new Error('ExpectectedPublicKeyForInboundPeerConstraintInRecovery');
92
+ }
93
+
77
94
  const claimSoloPublicKeyRecord = findRecord(records, typeClaimSoloPublicKey);
78
95
 
79
96
  if (!claimSoloPublicKeyRecord) {
@@ -5,6 +5,7 @@ const {parsePaymentRequest} = require('ln-service');
5
5
  const encodeSwapSecrets = require('./encode_swap_secrets');
6
6
  const {requestAsRequestRecords} = require('./../records');
7
7
  const {publicTypes} = require('./swap_field_types');
8
+ const {swapVersion} = require('./swap_field_types');
8
9
 
9
10
  const encode = records => encodeTlvStream({records}).encoded;
10
11
  const encodeNumber = n => encodeBigSize({number: n.toString()}).encoded;
@@ -13,6 +14,7 @@ const {typeClaimCoopPublicKeyHash} = publicTypes;
13
14
  const {typeClaimSoloPublicKey} = publicTypes;
14
15
  const {typeDeposit} = publicTypes;
15
16
  const {typeHash} = publicTypes;
17
+ const {typeInboundPeer} = publicTypes;
16
18
  const {typePush} = publicTypes;
17
19
  const {typePrivateRefundDetails} = publicTypes;
18
20
  const {typeRefundCoopPrivateKeyHash} = publicTypes;
@@ -21,6 +23,7 @@ const {typeRefundSoloPublicKey} = publicTypes;
21
23
  const {typeRequest} = publicTypes;
22
24
  const {typeTimeout} = publicTypes;
23
25
  const {typeTokens} = publicTypes;
26
+ const {typeVersion} = publicTypes;
24
27
 
25
28
  /** Serialize off to on swap records
26
29
 
@@ -32,6 +35,7 @@ const {typeTokens} = publicTypes;
32
35
  deposit: <Deposit BOLT 11 Request String>
33
36
  encrypt: <Encrypt Secrets Base Encryption Key Hex String>
34
37
  hash: <Swap Hash Hex String>
38
+ [incoming_peer]: <Constrained to Inbound Peer Public Key Id Hex String>
35
39
  [key_index]: <Refund Unilateral Key Id Number>
36
40
  push: <BOLT 11 Encoded Push Request String>
37
41
  refund_public_key: <Refund Unilateral Public Key Hex String>
@@ -61,6 +65,10 @@ module.exports = args => {
61
65
  type: typeDeposit,
62
66
  value: deposit.payment + depositAmount,
63
67
  },
68
+ {
69
+ type: typeInboundPeer,
70
+ value: args.incoming_peer,
71
+ },
64
72
  {
65
73
  type: typeRefundCoopPrivateKeyHash,
66
74
  value: privateCoopKeyHash,
@@ -85,6 +93,10 @@ module.exports = args => {
85
93
  type: typeTimeout,
86
94
  value: timeout,
87
95
  },
96
+ {
97
+ type: typeVersion,
98
+ value: encodeNumber(swapVersion),
99
+ },
88
100
  ];
89
101
 
90
102
  const {encoded} = encodeSwapSecrets({
@@ -108,6 +120,10 @@ module.exports = args => {
108
120
  type: typeHash,
109
121
  value: swapHash,
110
122
  },
123
+ {
124
+ type: typeInboundPeer,
125
+ value: args.incoming_peer,
126
+ },
111
127
  {
112
128
  type: typePrivateRefundDetails,
113
129
  value: encoded,
@@ -124,10 +140,14 @@ module.exports = args => {
124
140
  type: typeTokens,
125
141
  value: encodeNumber(args.tokens),
126
142
  },
143
+ {
144
+ type: typeVersion,
145
+ value: encodeNumber(swapVersion),
146
+ },
127
147
  ];
128
148
 
129
149
  return {
130
- recovery: encode(recoveryRecords),
131
- response: encode(responseRecords),
150
+ recovery: encode(recoveryRecords.filter(n => !!n.value)),
151
+ response: encode(responseRecords.filter(n => !!n.value)),
132
152
  };
133
153
  };
@@ -1,3 +1,4 @@
1
+ const {address} = require('bitcoinjs-lib');
1
2
  const asyncAuto = require('async/auto');
2
3
  const asyncMap = require('async/map');
3
4
  const {findKey} = require('ln-sync');
@@ -6,6 +7,7 @@ const {getAllInvoices} = require('ln-sync');
6
7
  const {getInvoice} = require('ln-service');
7
8
  const {getNetwork} = require('ln-sync');
8
9
  const {getNodeAlias} = require('ln-sync');
10
+ const {networks} = require('bitcoinjs-lib');
9
11
  const {returnResult} = require('asyncjs-util');
10
12
 
11
13
  const decodeOffToOnRequest = require('./decode_off_to_on_request');
@@ -25,6 +27,7 @@ const pushesReceivedAfter = () => new Date(Date.now() - 1000 * 60 * 60 * 24);
25
27
  const recoverRespondAction = 'recover-response';
26
28
  const requestAction = 'request';
27
29
  const respondAction = 'respond';
30
+ const {toOutputScript} = address;
28
31
  const tokensAsBigUnit = tokens => (tokens / 1e8).toFixed(8);
29
32
  const typeKeySendTrade = '805805';
30
33
 
@@ -260,14 +263,57 @@ module.exports = (args, cbk) => {
260
263
  }
261
264
  }],
262
265
 
266
+ // Ask if the sweep address should be custom
267
+ askForSweepAddress: [
268
+ 'askForExternal',
269
+ 'getNetwork',
270
+ 'selectAction',
271
+ ({getNetwork, selectAction}, cbk) =>
272
+ {
273
+ switch (selectAction) {
274
+ case actionPushRequest:
275
+ case requestAction:
276
+ return args.ask({
277
+ message: 'Send on-chain funds to an external address? (Optional)',
278
+ name: 'sweep',
279
+ type: 'input',
280
+ validate: input => {
281
+ if (!input) {
282
+ return true;
283
+ }
284
+
285
+ try {
286
+ return !!toOutputScript(input, networks[getNetwork.bitcoinjs]);
287
+ } catch (err) {
288
+ return 'Unsupported on-chain address format';
289
+ }
290
+ }
291
+ },
292
+ ({sweep}) => cbk(null, sweep));
293
+
294
+ default:
295
+ // Custom sweep address not supported
296
+ return cbk();
297
+ }
298
+ }],
299
+
263
300
  // Make swap request
264
301
  makeRequest: [
265
302
  'askForExternal',
266
303
  'askForRemote',
304
+ 'askForSweepAddress',
267
305
  'findKey',
268
306
  'hasTr',
269
307
  'selectAction',
270
- ({askForExternal, askForRemote, findKey, hasTr, selectAction}, cbk) =>
308
+ ({
309
+ askForExternal,
310
+ askForRemote,
311
+ askForSweepAddress,
312
+ findKey,
313
+ hasTr,
314
+ selectAction,
315
+ },
316
+ cbk) =>
271
317
  {
272
318
  switch (selectAction) {
273
319
  case actionPushRequest:
@@ -291,6 +337,7 @@ module.exports = (args, cbk) => {
291
337
  push_to: findKey.public_key,
292
338
  min_confirmations: minConfirmations[getNetwork.network],
293
339
  request: !hasTr ? args.request : undefined,
340
+ sweep_address: askForSweepAddress || undefined,
294
341
  },
295
342
  cbk);
296
343
  }],
@@ -51,6 +51,7 @@ const typeSwapResponse = serviceTypes.serviceTypeSwapResponse;
51
51
  [min_confirmations]: <Minimum Confirmations to Wait Number>
52
52
  [push_to]: <Push Swap Request to Node with Identity Public Key Hex String>
53
53
  [request]: <Request Function>
54
+ [sweep_address]: <Sweep Chain Address String>
54
55
  }
55
56
 
56
57
  @returns via cbk or Promise
@@ -329,6 +330,7 @@ module.exports = (args, cbk) => {
329
330
  });
330
331
 
331
332
  const deposit = mtokensAsTokens(response.deposit_mtokens);
333
+ const inbound = response.incoming_peer;
332
334
  const timeout = `that times out at ${response.timeout}`;
333
335
  const {tokens} = parsePaymentRequest({request: response.request});
334
336
 
@@ -340,9 +342,11 @@ module.exports = (args, cbk) => {
340
342
  return cbk(null, true)
341
343
  }
342
344
 
345
+ const inPeer = !!args.is_external_funding ? ` in via ${inbound}` : '';
346
+
343
347
  return args.ask({
344
348
  default: true,
345
- message: `Start swap ${timeout}? ${pricing}?`,
349
+ message: `Start swap ${timeout}? ${pricing}${inPeer}?`,
346
350
  name: 'ok',
347
351
  type: 'confirm',
348
352
  },
@@ -378,6 +382,7 @@ module.exports = (args, cbk) => {
378
382
  recovery: makeRequest.recovery,
379
383
  request: args.request,
380
384
  response: getResponse,
385
+ sweep_address: args.sweep_address,
381
386
  },
382
387
  cbk);
383
388
  }],
@@ -2,6 +2,7 @@ const {createHash} = require('crypto');
2
2
  const EventEmitter = require('events');
3
3
 
4
4
  const asyncAuto = require('async/auto');
5
+ const {findKey} = require('ln-sync');
5
6
  const {getChainFeeRate} = require('ln-service');
6
7
  const {returnResult} = require('asyncjs-util');
7
8
 
@@ -158,6 +159,24 @@ module.exports = ({ask, lnd, logger, request, swap, to}, cbk) => {
158
159
  ({target}) => cbk(null, Number(target)));
159
160
  }],
160
161
 
162
+ // Ask for an incoming peer to constrain the swap to
163
+ askForIncoming: ['askForTarget', ({}, cbk) => {
164
+ return ask({
165
+ message: 'Require off-chain through a specific peer? (Optional)',
166
+ name: 'incoming',
167
+ },
168
+ ({incoming}) => cbk(null, incoming));
169
+ }],
170
+
171
+ // Find the incoming identity key when specified
172
+ findIncoming: ['askForIncoming', ({askForIncoming}, cbk) => {
173
+ if (!askForIncoming) {
174
+ return cbk(null, {});
175
+ }
176
+
177
+ return findKey({lnd, query: askForIncoming}, cbk);
178
+ }],
179
+
161
180
  // Get the chain fee rate
162
181
  getRate: ['askForTarget', ({askForTarget}, cbk) => {
163
182
  return getChainFeeRate({lnd, confirmation_target: askForTarget}, cbk);
@@ -167,15 +186,21 @@ module.exports = ({ask, lnd, logger, request, swap, to}, cbk) => {
167
186
  makeResponse: [
168
187
  'askForRate',
169
188
  'askForRequest',
189
+ 'findIncoming',
170
190
  'getRate',
171
- ({askForRate, askForRequest, getRate}, cbk) =>
191
+ ({askForRate, askForRequest, findIncoming, getRate}, cbk) =>
172
192
  {
173
193
  const {tokens} = decodeOffToOnRequest({request: askForRequest});
174
194
 
195
+ if (!!findIncoming.public_key) {
196
+ logger.info({incoming_peer_constraint: findIncoming.public_key});
197
+ }
198
+
175
199
  return startOnToOffSwap({
176
200
  lnd,
177
201
  delta: defaultCltvDelta,
178
202
  deposit: ceil(getRate.tokens_per_vbyte * estimatedVirtualSize),
203
+ incoming_peer: findIncoming.public_key || undefined,
179
204
  is_external_solo_key: !!request,
180
205
  price: floor(tokens * askForRate / rateDenominator),
181
206
  request: askForRequest,
@@ -29,6 +29,7 @@ const sha256 = preimage => createHash('sha256').update(preimage).digest('hex');
29
29
  {
30
30
  delta: <Swap CLTV Delta Number>
31
31
  deposit: <Unilateral Deposit Tokens Number>
32
+ [incoming_peer]: <Constrained to Inbound Peer Public Key Id Hex String>
32
33
  [is_external_solo_key]: <Use External Unilateral Refund Key Bool>
33
34
  lnd: <Authenticated LND API Object>
34
35
  price: <Swap Price Tokens Number>
@@ -227,6 +228,7 @@ module.exports = (args, cbk) => {
227
228
  deposit: createExecInvoice.request,
228
229
  encrypt: getEncrypt.secret,
229
230
  hash: requestDetails.hash,
231
+ incoming_peer: args.incoming_peer || undefined,
230
232
  key_index: getRefundKey.index,
231
233
  push: createPushInvoice.request,
232
234
  refund_public_key: getRefundKey.public_key,
@@ -8,6 +8,7 @@
8
8
  "typeSoloPrivateKey": "6"
9
9
  },
10
10
  "publicTypes": {
11
+ "typeVersion": "0",
11
12
  "typeClaimCoopPublicKeyHash": "1",
12
13
  "typeClaimSoloPublicKey": "2",
13
14
  "typeDeposit": "3",
@@ -21,11 +22,12 @@
21
22
  "typeRequest": "11",
22
23
  "typeTimeout": "12",
23
24
  "typeTokens": "13",
24
- "typeVersion": "0"
25
+ "typeInboundPeer": "14"
25
26
  },
26
27
  "pushTypes": {
27
28
  "typeSwapId": "0",
28
29
  "typeSwapResponse": "1"
29
30
  },
31
+ "swapVersion": 1,
30
32
  "typePayMetadata": "805001"
31
33
  }
@@ -103,6 +103,10 @@ test(`Start offchain swap`, async ({end, equal, strictSame}) => {
103
103
  return cbk({[args.name]: args.default});
104
104
  }
105
105
 
106
+ if (args.name === 'incoming') {
107
+ return cbk({incoming: false});
108
+ }
109
+
106
110
  if (args.name === 'req') {
107
111
  return cbk({req: swapRequest.swap_request});
108
112
  }
@@ -103,6 +103,10 @@ test(`Swap with claim path`, async ({end, equal, strictSame}) => {
103
103
  return cbk({[args.name]: args.default});
104
104
  }
105
105
 
106
+ if (args.name === 'incoming') {
107
+ return cbk({incoming: false});
108
+ }
109
+
106
110
  if (args.name === 'req') {
107
111
  return cbk({req: swapRequest.swap_request});
108
112
  }
@@ -104,6 +104,10 @@ test(`Timeout a swap`, async ({end, equal, strictSame}) => {
104
104
  return cbk({[args.name]: args.default});
105
105
  }
106
106
 
107
+ if (args.name === 'incoming') {
108
+ return cbk({incoming: false});
109
+ }
110
+
107
111
  if (args.name === 'req') {
108
112
  return cbk({req: swapRequest.swap_request});
109
113
  }
@@ -2,12 +2,15 @@ const {addPeer} = require('ln-service');
2
2
  const asyncAuto = require('async/auto');
3
3
  const asyncDetect = require('async/detect');
4
4
  const asyncDetectSeries = require('async/detectSeries');
5
+ const asyncEach = require('async/each');
5
6
  const asyncMap = require('async/map');
6
7
  const asyncRetry = require('async/retry');
7
8
  const {getChannel} = require('ln-service');
9
+ const {getChannels} = require('ln-service');
8
10
  const {getIdentity} = require('ln-service');
9
11
  const {getNode} = require('ln-service');
10
12
  const {getPeers} = require('ln-service');
13
+ const {removePeer} = require('ln-service');
11
14
  const {returnResult} = require('asyncjs-util');
12
15
 
13
16
  const interval = 1000;
@@ -55,12 +58,14 @@ module.exports = ({lnd, logger, nodes}, cbk) => {
55
58
  return cbk();
56
59
  },
57
60
 
61
+ // Get the channels to see if this is a channel peer
62
+ getChannels: ['validate', ({}, cbk) => {
63
+ return getChannels({lnd, is_active: true}, cbk);
64
+ }],
65
+
58
66
  // Derive the self public key
59
67
  getIdentity: ['validate', ({}, cbk) => getIdentity({lnd}, cbk)],
60
68
 
61
- // Get the list of connected peers
62
- getPeers: ['validate', ({}, cbk) => getPeers({lnd}, cbk)],
63
-
64
69
  // Find node connect info to connect to
65
70
  getNodes: ['getIdentity', ({getIdentity}, cbk) => {
66
71
  return asyncMap(nodes, (connect, cbk) => {
@@ -135,8 +140,56 @@ module.exports = ({lnd, logger, nodes}, cbk) => {
135
140
  cbk);
136
141
  }],
137
142
 
143
+ // Remove the peer if there is no channel peer to avoid stale peers
144
+ removePeer: [
145
+ 'getChannels',
146
+ 'getNodes',
147
+ ({getChannels, getNodes}, cbk) =>
148
+ {
149
+ const ids = getChannels.channels.map(n => n.partner_public_key);
150
+
151
+ return asyncEach(getNodes, (node, cbk) => {
152
+ // Exit early when there is no node id
153
+ if (!node || !node.id || ids.includes(node.id)) {
154
+ return cbk();
155
+ }
156
+
157
+ return removePeer({lnd, public_key: node.id}, (err, res) => {
158
+ if (!!err) {
159
+ return cbk(err);
160
+ }
161
+
162
+ return asyncRetry({interval, times}, cbk => {
163
+ return getPeers({lnd}, (err, res) => {
164
+ if (!!err) {
165
+ return cbk(err);
166
+ }
167
+
168
+ const peers = res.peers.map(n => n.public_key);
169
+
170
+ if (!!peers.includes(node.id)) {
171
+ return cbk([503, 'FailedToDisconnectSellerPeer']);
172
+ }
173
+
174
+ return cbk();
175
+ });
176
+ },
177
+ cbk);
178
+ });
179
+ },
180
+ cbk);
181
+ }],
182
+
183
+ // Get the list of connected peers
184
+ getPeers: ['removePeer', ({}, cbk) => getPeers({lnd}, cbk)],
185
+
138
186
  // Try and connect to a node in order to do p2p messaging
139
- connect: ['getNodes', 'getPeers', ({getNodes, getPeers}, cbk) => {
187
+ connect: [
188
+ 'getNodes',
189
+ 'getPeers',
190
+ 'removePeer',
191
+ ({getNodes, getPeers}, cbk) =>
192
+ {
140
193
  const connected = getPeers.peers.map(n => n.public_key);
141
194
 
142
195
  // Look for a node that is already a connected peer