paid-services 3.16.3 → 3.17.2

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 (50) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/groups/ask_for_group_details.js +160 -0
  3. package/groups/ask_to_confirm_group_join.js +140 -0
  4. package/groups/assemble/assemble_unsigned_psbt.js +169 -0
  5. package/groups/assemble/coordinate_group.js +383 -0
  6. package/groups/assemble/index.js +3 -0
  7. package/groups/assemble_channel_group.js +178 -0
  8. package/groups/attach_to_channel_group.js +80 -0
  9. package/groups/coordinate_channel_group.js +122 -0
  10. package/groups/funding/confirm_incoming_channel.js +108 -0
  11. package/groups/funding/index.js +9 -0
  12. package/groups/funding/propose_group_channel.js +167 -0
  13. package/groups/funding/sign_and_fund_group_channel.js +188 -0
  14. package/groups/index.js +3 -0
  15. package/groups/join_channel_group.js +193 -0
  16. package/groups/manage_group_join.js +96 -0
  17. package/groups/members/index.js +3 -0
  18. package/groups/members/partners_from_members.js +26 -0
  19. package/groups/messages/decode_connected_records.js +56 -0
  20. package/groups/messages/decode_group_details.js +99 -0
  21. package/groups/messages/decode_partners_records.js +57 -0
  22. package/groups/messages/decode_pending_proposal.js +167 -0
  23. package/groups/messages/decode_signed_funding.js +42 -0
  24. package/groups/messages/decode_signed_records.js +56 -0
  25. package/groups/messages/decode_unsigned_funding.js +43 -0
  26. package/groups/messages/encode_connected_records.js +26 -0
  27. package/groups/messages/encode_group_details.js +32 -0
  28. package/groups/messages/encode_partners_records.js +22 -0
  29. package/groups/messages/encode_pending_proposal.js +91 -0
  30. package/groups/messages/encode_signed_records.js +26 -0
  31. package/groups/messages/encode_unsigned_funding.js +19 -0
  32. package/groups/messages/index.js +29 -0
  33. package/groups/p2p/find_group_partners.js +122 -0
  34. package/groups/p2p/get_group_details.js +81 -0
  35. package/groups/p2p/index.js +15 -0
  36. package/groups/p2p/peer_with_partners.js +57 -0
  37. package/groups/p2p/register_group_connected.js +107 -0
  38. package/groups/p2p/register_pending_open.js +218 -0
  39. package/groups/p2p/register_signed_open.js +118 -0
  40. package/index.js +2 -0
  41. package/package.json +3 -3
  42. package/service_types.json +11 -1
  43. package/test/integration/test_group.js +174 -0
  44. package/trades/buy_channel.js +1 -1
  45. package/trades/buy_preimage.js +1 -1
  46. package/trades/create_channel_sale.js +6 -6
  47. package/trades/create_trade.js +7 -7
  48. package/trades/find_trade.js +1 -1
  49. package/trades/manage_trade.js +4 -4
  50. package/trades/manage_trades.js +3 -3
@@ -0,0 +1,383 @@
1
+ const EventEmitter = require('events');
2
+ const {randomBytes} = require('crypto');
3
+
4
+ const assembleUnsignedPsbt = require('./assemble_unsigned_psbt');
5
+ const {decodeGroupDetails} = require('./../messages');
6
+ const {decodePendingProposal} = require('./../messages');
7
+ const {decodeSignedFunding} = require('./../messages');
8
+ const {encodeConnectedRecords} = require('./../messages');
9
+ const {encodeGroupDetails} = require('./../messages');
10
+ const {encodeSignedRecords} = require('./../messages');
11
+ const {encodeUnsignedFunding} = require('./../messages');
12
+ const {encodePartnersRecords} = require('./../messages');
13
+ const {partnersFromMembers} = require('./../members');
14
+ const {servicePeerRequests} = require('./../../p2p');
15
+ const {serviceTypeConfirmConnected} = require('./../../service_types');
16
+ const {serviceTypeFindGroupPartners} = require('./../../service_types');
17
+ const {serviceTypeGetGroupDetails} = require('./../../service_types');
18
+ const {serviceTypeRegisterPendingOpen} = require('./../../service_types');
19
+ const {serviceTypeRegisterSignedOpen} = require('./../../service_types');
20
+
21
+ const findRecord = (records, type) => records.find(n => n.type === type);
22
+ const {isArray} = Array;
23
+ const makeGroupId = () => randomBytes(16).toString('hex');
24
+ const minGroupCount = 3;
25
+ const now = () => new Date().toISOString();
26
+ const staleDate = () => new Date(Date.now() - (1000 * 60 * 10)).toISOString();
27
+ const typeGroupId = '1';
28
+
29
+ /** Coordinate channel group
30
+
31
+ {
32
+ capacity: <Channel Capacity Tokens Number>
33
+ count: <Group Members Count Number>
34
+ identity: <Coordinator Identity Public Key Hex String>
35
+ lnd: <Authenticated LND API Object>
36
+ rate: <Chain Fee Rate Number>
37
+ }
38
+
39
+ @returns
40
+ {
41
+ events: <Event Emitter Object>
42
+ id: <Group Id Hex String>
43
+ connected: <Add Connected Identity Public Key Hex String>
44
+ partners: <Get Channel Partners Function>
45
+ proposed: <Add Proposed Channel Function>
46
+ sign: <Add Signed Channel Function>
47
+ signed: <Get Signed PSBTs Function>
48
+ unsigned: <Get Unsigned PSBT Function>
49
+ }
50
+
51
+ // All members are connected
52
+ @event 'connected'
53
+
54
+ // A member was connected to their peers
55
+ @event 'connecting'
56
+ {
57
+ id: <Connected Member Public Key Hex String>
58
+ }
59
+
60
+ // All members have funded with their outbound peers
61
+ @event 'funded'
62
+
63
+ // A member has funded with their outbound peer
64
+ @event 'funding'
65
+ {
66
+ id: <Funding Member Public Key Hex String>
67
+ }
68
+
69
+ // A new member joined
70
+ @event 'joining'
71
+ {
72
+ id: <Joined Member Public Key Hex String>
73
+ }
74
+
75
+ // All members are joined
76
+ @event 'joined'
77
+ {
78
+ ids: [<Joined Member Public Key Hex String>]
79
+ }
80
+
81
+ // A member submitted their partial signature
82
+ @event 'signing'
83
+ {
84
+ id: <Member Public Key Hex String>
85
+ }
86
+
87
+ // All members have submitted their partial signatures
88
+ @event 'signed'
89
+ */
90
+ module.exports = ({capacity, count, identity, lnd, rate}) => {
91
+ if (count < minGroupCount) {
92
+ throw new Error('ExpectedHigherGroupMembersCountToCoordinateGroup');
93
+ }
94
+
95
+ if (!identity) {
96
+ throw new Error('ExpectedSelfIdentityPublicKeyToCoordinateGroup');
97
+ }
98
+
99
+ if (!lnd) {
100
+ throw new Error('ExpectedAuthenticatedLndToCoordinateGroup');
101
+ }
102
+
103
+ // Instantiate the group with self as a member
104
+ const group = {
105
+ connected: [],
106
+ emitter: new EventEmitter(),
107
+ members: [{id: identity}],
108
+ proposed: [],
109
+ signed: [],
110
+ };
111
+
112
+ // The group has an identifier
113
+ const id = makeGroupId();
114
+
115
+ // Wait for peer requests
116
+ const service = servicePeerRequests({lnd});
117
+
118
+ // Listen for and respond to get group details requests
119
+ service.request({type: serviceTypeGetGroupDetails}, (req, res) => {
120
+ if (!isArray(req.records)) {
121
+ return res.failure([400, 'ExpectedArrayOfRecordsToGetGroupDetails']);
122
+ }
123
+
124
+ const idRecord = findRecord(req.records, typeGroupId);
125
+
126
+ if (!idRecord) {
127
+ return res.failure([400, 'ExpectedGroupIdRecordToGetGroupDetails']);
128
+ }
129
+
130
+ // Exit early when this request is for a different group
131
+ if (idRecord.value !== id) {
132
+ return;
133
+ }
134
+
135
+ return res.success(encodeGroupDetails({capacity, count, rate}));
136
+ });
137
+
138
+ // Listen for and respond to find member requests
139
+ service.request({type: serviceTypeFindGroupPartners}, (req, res) => {
140
+ if (!isArray(req.records)) {
141
+ return res.failure([400, 'ExpectedArrayOfRecordsToFindGroupPartners']);
142
+ }
143
+
144
+ const idRecord = findRecord(req.records, typeGroupId);
145
+
146
+ if (!idRecord) {
147
+ return res.failure([400, 'ExpectedGroupIdRecordToFindGroupPartners']);
148
+ }
149
+
150
+ // Exit early when this request is for a different group
151
+ if (idRecord.value !== id) {
152
+ return;
153
+ }
154
+
155
+ // Emit event that someone is joining
156
+ if (!group.members.find(n => n.id === req.from)) {
157
+ group.emitter.emit('joining', {id: req.from});
158
+ }
159
+
160
+ // Refresh the member list, evicting members who stopped pinging
161
+ group.members = group.members
162
+ .filter(n => !n.last_joined || n.last_joined > staleDate())
163
+ .filter(n => n.id !== req.from);
164
+
165
+ // Exit early with failure when the group is full
166
+ if (group.members.length === count) {
167
+ return res.failure([503, 'GroupIsCurrentlyFull']);
168
+ }
169
+
170
+ // Add the group member details to the group
171
+ group.members.push({id: req.from, last_joined: new Date().toISOString()});
172
+
173
+ // Exit with failure when a member dropped out after being locked in
174
+ if (!!group.ids && group.members.length < count) {
175
+ return res.failure([503, 'GroupMemberExited']);
176
+ }
177
+
178
+ // Exit early when still waiting for group to fill
179
+ if (group.members.length < count) {
180
+ return res.success({});
181
+ }
182
+
183
+ // Make sure members list is locked in
184
+ group.ids = group.ids || group.members.map(n => n.id);
185
+
186
+ // Emit event that everyone has joined
187
+ group.emitter.emit('joined', {ids: group.ids});
188
+
189
+ // Derive position in members list
190
+ const {inbound, outbound} = partnersFromMembers({group, id: req.from});
191
+
192
+ // Encode partners for wire
193
+ const {records} = encodePartnersRecords({inbound, outbound});
194
+
195
+ return res.success({records});
196
+ });
197
+
198
+ // Listen for and respond to connected confirmations
199
+ service.request({type: serviceTypeConfirmConnected}, (req, res) => {
200
+ if (!isArray(req.records)) {
201
+ return res.failure([400, 'ExpectedArrayOfRecordsToConfirmConnection']);
202
+ }
203
+
204
+ const idRecord = findRecord(req.records, typeGroupId);
205
+
206
+ if (!idRecord) {
207
+ return res.failure([400, 'ExpectedGroupIdRecordToConfirmConnection']);
208
+ }
209
+
210
+ // Exit early when this request is for a different group
211
+ if (idRecord.value !== id) {
212
+ return;
213
+ }
214
+
215
+ // Group ids must be locked in before connections start
216
+ if (!group.ids) {
217
+ return res.failure([503, 'GroupMembersAreNotLockedInToStartConnects']);
218
+ }
219
+
220
+ // Cannot confirm connected when not part of the locked in group
221
+ if (!group.ids.includes(req.from)) {
222
+ return res.failure([403, 'MembershipNotPresentInLockedInMembers']);
223
+ }
224
+
225
+ // Emit event that someone is connecting
226
+ if (!group.connected.find(n => n.id === req.from)) {
227
+ group.emitter.emit('connecting', {id: req.from});
228
+ }
229
+
230
+ // Refresh the connected list, evicting remote members who stopped pinging
231
+ group.connected = group.connected
232
+ .filter(n => !n.last_connected || n.last_connected > staleDate())
233
+ .filter(n => n.id !== req.from);
234
+
235
+ // Add the group connection details to the group
236
+ group.connected.push({id: req.from, last_connected: now()});
237
+
238
+ const {records} = encodeConnectedRecords({count: group.connected.length});
239
+
240
+ // Emit event that all members are connected
241
+ if (group.connected.length === count) {
242
+ group.emitter.emit('connected', {});
243
+ }
244
+
245
+ return res.success({records});
246
+ });
247
+
248
+ // Listen for and respond to pending open registrations
249
+ service.request({type: serviceTypeRegisterPendingOpen}, (req, res) => {
250
+ if (!isArray(req.records)) {
251
+ return res.failure([400, 'ExpectedArrayOfRecordsToRegisterPendingOpen']);
252
+ }
253
+
254
+ const idRecord = findRecord(req.records, typeGroupId);
255
+
256
+ if (!idRecord) {
257
+ return res.failure([400, 'ExpectedGroupIdRecordToRegisterPendingOpen']);
258
+ }
259
+
260
+ // Exit early when this request is for a different group
261
+ if (idRecord.value !== id) {
262
+ return;
263
+ }
264
+
265
+ // Cannot register pending when not part of the locked in group
266
+ if (!group.ids.includes(req.from)) {
267
+ return res.failure([403, 'MembershipNotPresentInLockedInMembers']);
268
+ }
269
+
270
+ try {
271
+ decodePendingProposal({records: req.records});
272
+ } catch (err) {
273
+ return res.failure([400, err.message]);
274
+ }
275
+
276
+ const proposal = decodePendingProposal({records: req.records});
277
+
278
+ // Register the proposal
279
+ if (!group.proposed.find(n => n.id === req.from)) {
280
+ group.emitter.emit('funding', {id: req.from});
281
+
282
+ group.proposed.push({
283
+ change: proposal.change,
284
+ funding: proposal.funding,
285
+ id: req.from,
286
+ utxos: proposal.utxos,
287
+ });
288
+ }
289
+
290
+ // Exit early when there are missing proposals
291
+ if (group.proposed.length !== group.ids.length) {
292
+ return res.success({});
293
+ }
294
+
295
+ const {failure, success} = res;
296
+
297
+ // Exit early when unsigned funding is already assembled
298
+ if (!!group.unsigned) {
299
+ return success(encodeUnsignedFunding({psbt: group.unsigned}));
300
+ }
301
+
302
+ // Put together the assembled PSBT
303
+ return assembleUnsignedPsbt({
304
+ capacity,
305
+ rate,
306
+ proposed: group.proposed,
307
+ },
308
+ (err, res) => {
309
+ if (!!err) {
310
+ return failure(err);
311
+ }
312
+
313
+ group.unsigned = res.psbt;
314
+
315
+ if (group.proposed.length === count) {
316
+ group.emitter.emit('funded', {});
317
+ }
318
+
319
+ const {records} = encodeUnsignedFunding({psbt: res.psbt});
320
+
321
+ return success({records});
322
+ });
323
+ });
324
+
325
+ // Listen for and respond to funding signatures
326
+ service.request({type: serviceTypeRegisterSignedOpen}, (req, res) => {
327
+ if (!isArray(req.records)) {
328
+ return res.failure([400, 'ExpectedArrayOfRecordsToRegisterSignedOpen']);
329
+ }
330
+
331
+ const idRecord = findRecord(req.records, typeGroupId);
332
+
333
+ if (!idRecord) {
334
+ return res.failure([400, 'ExpectedGroupIdRecordToRegisterSignedOpen']);
335
+ }
336
+
337
+ // Exit early when this request is for a different group
338
+ if (idRecord.value !== id) {
339
+ return;
340
+ }
341
+
342
+ // Cannot register signatures when not part of the locked in group
343
+ if (!group.ids.includes(req.from)) {
344
+ return res.failure([403, 'MembershipNotPresentInLockedInMembers']);
345
+ }
346
+
347
+ try {
348
+ decodeSignedFunding({records: req.records});
349
+ } catch (err) {
350
+ return res.failure([400, err.message]);
351
+ }
352
+
353
+ const signed = decodeSignedFunding({records: req.records});
354
+
355
+ // Register the signed funding
356
+ if (!group.signed.find(n => n.id === req.from)) {
357
+ // Emit event to indicate member has submitted their signed open
358
+ group.emitter.emit('signing', {id: req.from});
359
+
360
+ group.signed.push({id: req.from, signed: signed.psbt});
361
+ }
362
+
363
+ if (group.signed.length === count) {
364
+ group.emitter.emit('signed', {});
365
+ }
366
+
367
+ // Let the client know how many signatures are present
368
+ const {records} = encodeSignedRecords({count: group.signed.length});
369
+
370
+ return res.success({records});
371
+ });
372
+
373
+ return {
374
+ id,
375
+ connected: () => group.connected.push({id: identity}),
376
+ events: group.emitter,
377
+ partners: id => !!group.ids ? partnersFromMembers({group, id}) : undefined,
378
+ proposed: pending => group.proposed.push(pending),
379
+ sign: signed => group.signed.push(signed),
380
+ signed: () => group.signed,
381
+ unsigned: () => group.unsigned,
382
+ };
383
+ };
@@ -0,0 +1,3 @@
1
+ const coordinateGroup = require('./coordinate_group');
2
+
3
+ module.exports = {coordinateGroup};
@@ -0,0 +1,178 @@
1
+ const EventEmitter = require('events');
2
+
3
+ const asyncRetry = require('async/retry');
4
+ const {broadcastChainTransaction} = require('ln-service');
5
+ const {combinePsbts} = require('psbt');
6
+ const {decodePsbt} = require('psbt');
7
+ const {extractTransaction} = require('psbt');
8
+ const {finalizePsbt} = require('psbt');
9
+ const {Transaction} = require('bitcoinjs-lib');
10
+
11
+ const {confirmIncomingChannel} = require('./funding');
12
+ const {coordinateGroup} = require('./assemble');
13
+ const {peerWithPartners} = require('./p2p');
14
+ const {proposeGroupChannel} = require('./funding');
15
+ const {signAndFundGroupChannel} = require('./funding');
16
+
17
+ const {fromHex} = Transaction;
18
+ const interval = 500;
19
+ const times = 2 * 60 * 10;
20
+
21
+ /** Assemble a channel group
22
+
23
+ {
24
+ capacity: <Channel Capacity Tokens Number>
25
+ count: <Channel Members Count Number>
26
+ ecp: <ECPair Library Object>
27
+ identity: <Coordinator Identity Public Key Hex String>
28
+ lnd: <Authenticated LND API Object>
29
+ rate: <Chain Fee Tokens Per VByte Number>
30
+ }
31
+
32
+ @returns
33
+ {
34
+ events: <EventEmitter Object>
35
+ id: <Group Id Hex String>
36
+ }
37
+
38
+ // Open was published
39
+ @event 'broadcast'
40
+ {
41
+ id: <Transaction Id Hex String>
42
+ transaction: <Transaction Hex String>
43
+ }
44
+
45
+ // Open is publishing
46
+ @event 'broadcasting'
47
+ {
48
+ id: <Transaction Id Hex String>
49
+ transaction: <Transaction Hex String>
50
+ }
51
+
52
+ // Members are peered
53
+ @event 'connected'
54
+ {}
55
+
56
+ // All members are present
57
+ @event 'filled'
58
+ {
59
+ ids: [<Group Member Identity Public Key Hex String>]
60
+ }
61
+
62
+ // Members proposed channels to each other
63
+ @event 'proposed'
64
+ {}
65
+
66
+ // Members have all signed
67
+ @event 'signed'
68
+ {}
69
+ */
70
+ module.exports = ({capacity, count, ecp, identity, lnd, rate}) => {
71
+ const coordinator = coordinateGroup({capacity, count, identity, lnd, rate});
72
+ const emitter = new EventEmitter();
73
+ const pending = {};
74
+
75
+ const errored = err => {
76
+ coordinator.events.removeAllListeners();
77
+
78
+ return emitter.emit('error', err);
79
+ };
80
+
81
+ // Group members have registered themselves
82
+ coordinator.events.once('joined', async ({ids}) => {
83
+ emitter.emit('filled', {ids});
84
+
85
+ const {inbound, outbound} = coordinator.partners(identity);
86
+
87
+ // Connect to the inbound and outbound partners
88
+ try {
89
+ await peerWithPartners({inbound, lnd, outbound});
90
+
91
+ // Register as connected
92
+ return coordinator.connected();
93
+ } catch (err) {
94
+ return errored(err);
95
+ }
96
+ });
97
+
98
+ // Group members have connected to each other
99
+ coordinator.events.once('connected', async () => {
100
+ emitter.emit('connected', {});
101
+
102
+ try {
103
+ // Fund and propose the pending channel to the outbound partner
104
+ const {change, funding, id, utxos} = await proposeGroupChannel({
105
+ capacity,
106
+ lnd,
107
+ rate,
108
+ to: coordinator.partners(identity).outbound,
109
+ });
110
+
111
+ pending.id = id;
112
+ pending.utxos = utxos;
113
+
114
+ // Register as proposed
115
+ return coordinator.proposed({change, funding, utxos, id: identity});
116
+ } catch (err) {
117
+ return errored(err);
118
+ }
119
+ });
120
+
121
+ // Group members have proposed channels to each other
122
+ coordinator.events.once('funded', async () => {
123
+ emitter.emit('proposed', {});
124
+
125
+ const basePsbt = decodePsbt({ecp, psbt: coordinator.unsigned()});
126
+
127
+ // Sign the unsigned funding transaction
128
+ const signed = await signAndFundGroupChannel({
129
+ lnd,
130
+ id: pending.id,
131
+ psbt: coordinator.unsigned(),
132
+ utxos: pending.utxos,
133
+ });
134
+
135
+ // Make sure there is an incoming channel
136
+ await asyncRetry({interval, times}, async () => {
137
+ return await confirmIncomingChannel({
138
+ capacity,
139
+ lnd,
140
+ from: coordinator.partners(identity).inbound,
141
+ id: fromHex(basePsbt.unsigned_transaction).getId(),
142
+ to: coordinator.partners(identity).outbound,
143
+ });
144
+ });
145
+
146
+ // Register the signature with the coordinator
147
+ return coordinator.sign({id: identity, signed: signed.psbt});
148
+ });
149
+
150
+ // Group members have submitted their partial signatures
151
+ coordinator.events.once('signed', async () => {
152
+ emitter.emit('signed', {});
153
+
154
+ // Collect all the partially signed PSBTs
155
+ const psbts = coordinator.signed().map(n => n.signed);
156
+
157
+ // Merge partial PSBTs into a single PSBT
158
+ const combined = combinePsbts({ecp, psbts});
159
+
160
+ // Finalize the PSBT to convert partial signatures to full final signatures
161
+ const finalized = finalizePsbt({ecp, psbt: combined.psbt});
162
+
163
+ // Pull out the raw transaction from the PSBT
164
+ const {transaction} = extractTransaction({ecp, psbt: finalized.psbt});
165
+
166
+ try {
167
+ emitter.emit('broadcasting', ({transaction}));
168
+
169
+ const {id} = await broadcastChainTransaction({lnd, transaction});
170
+
171
+ return emitter.emit('broadcast', {id, transaction});
172
+ } catch (err) {
173
+ return errored(err);
174
+ }
175
+ });
176
+
177
+ return {events: emitter, id: coordinator.id};
178
+ };
@@ -0,0 +1,80 @@
1
+ const asyncAuto = require('async/auto');
2
+ const asyncMap = require('async/map');
3
+ const {getNodeAlias} = require('ln-sync');
4
+ const {returnResult} = require('asyncjs-util');
5
+
6
+ const askToConfirmGroupJoin = require('./ask_to_confirm_group_join');
7
+ const joinChannelGroup = require('./join_channel_group');
8
+
9
+ const formatNodes = arr => arr.join(', ');
10
+ const niceName = ({alias, id}) => `${alias} ${id}`.trim();
11
+
12
+ /** Ask for a channel group to join and then join the group
13
+
14
+ {
15
+ ask: <Ask Function>
16
+ lnd: <Authenticated LND API Object>
17
+ }
18
+
19
+ @returns via cbk or Promise
20
+ {
21
+ id: <Transaction Id Hex String>
22
+ }
23
+ */
24
+ module.exports = ({ask, lnd, logger}, cbk) => {
25
+ return new Promise((resolve, reject) => {
26
+ return asyncAuto({
27
+ // Check arguments
28
+ validate: cbk => {
29
+ if (!ask) {
30
+ return cbk([400, 'ExpectedAskFunctionToEnterChannelGroup']);
31
+ }
32
+
33
+ if (!lnd) {
34
+ return cbk([400, 'ExpectedAuthenticatedLndToEnterChannelGroup']);
35
+ }
36
+
37
+ if (!logger) {
38
+ return cbk([400, 'ExpectedWinstonLoggerToAttachToChannelGroup']);
39
+ }
40
+
41
+ return cbk();
42
+ },
43
+
44
+ // Ask for group details to parse and confirm
45
+ askForDetails: ['validate', ({}, cbk) => {
46
+ return askToConfirmGroupJoin({ask, lnd}, cbk);
47
+ }],
48
+
49
+ // Join the channel group
50
+ join: ['askForDetails', ({askForDetails}, cbk) => {
51
+ logger.info({waiting_for_other_members: true});
52
+
53
+ const join = joinChannelGroup({
54
+ lnd,
55
+ capacity: askForDetails.capacity,
56
+ coordinator: askForDetails.coordinator,
57
+ count: askForDetails.count,
58
+ id: askForDetails.id,
59
+ rate: askForDetails.rate,
60
+ });
61
+
62
+ join.once('end', ({id}) => cbk(null, {transaction_id: id}));
63
+ join.once('error', err => cbk(err));
64
+
65
+ join.once('peering', async ({inbound, outbound}) => {
66
+ const nodes = await asyncMap([inbound, outbound], async id => {
67
+ return niceName(await getNodeAlias({id, lnd}));
68
+ });
69
+
70
+ return logger.info({peering_with: formatNodes(nodes)});
71
+ });
72
+
73
+ join.once('publishing', ({signed}) => logger.info({signed}));
74
+
75
+ return;
76
+ }],
77
+ },
78
+ returnResult({reject, resolve, of: 'join'}, cbk));
79
+ });
80
+ };