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.
- package/CHANGELOG.md +4 -0
- package/groups/ask_for_group_details.js +160 -0
- package/groups/ask_to_confirm_group_join.js +140 -0
- package/groups/assemble/assemble_unsigned_psbt.js +169 -0
- package/groups/assemble/coordinate_group.js +383 -0
- package/groups/assemble/index.js +3 -0
- package/groups/assemble_channel_group.js +178 -0
- package/groups/attach_to_channel_group.js +80 -0
- package/groups/coordinate_channel_group.js +122 -0
- package/groups/funding/confirm_incoming_channel.js +108 -0
- package/groups/funding/index.js +9 -0
- package/groups/funding/propose_group_channel.js +167 -0
- package/groups/funding/sign_and_fund_group_channel.js +188 -0
- package/groups/index.js +3 -0
- package/groups/join_channel_group.js +193 -0
- package/groups/manage_group_join.js +96 -0
- package/groups/members/index.js +3 -0
- package/groups/members/partners_from_members.js +26 -0
- package/groups/messages/decode_connected_records.js +56 -0
- package/groups/messages/decode_group_details.js +99 -0
- package/groups/messages/decode_partners_records.js +57 -0
- package/groups/messages/decode_pending_proposal.js +167 -0
- package/groups/messages/decode_signed_funding.js +42 -0
- package/groups/messages/decode_signed_records.js +56 -0
- package/groups/messages/decode_unsigned_funding.js +43 -0
- package/groups/messages/encode_connected_records.js +26 -0
- package/groups/messages/encode_group_details.js +32 -0
- package/groups/messages/encode_partners_records.js +22 -0
- package/groups/messages/encode_pending_proposal.js +91 -0
- package/groups/messages/encode_signed_records.js +26 -0
- package/groups/messages/encode_unsigned_funding.js +19 -0
- package/groups/messages/index.js +29 -0
- package/groups/p2p/find_group_partners.js +122 -0
- package/groups/p2p/get_group_details.js +81 -0
- package/groups/p2p/index.js +15 -0
- package/groups/p2p/peer_with_partners.js +57 -0
- package/groups/p2p/register_group_connected.js +107 -0
- package/groups/p2p/register_pending_open.js +218 -0
- package/groups/p2p/register_signed_open.js +118 -0
- package/index.js +2 -0
- package/package.json +3 -3
- package/service_types.json +11 -1
- package/test/integration/test_group.js +174 -0
- package/trades/buy_channel.js +1 -1
- package/trades/buy_preimage.js +1 -1
- package/trades/create_channel_sale.js +6 -6
- package/trades/create_trade.js +7 -7
- package/trades/find_trade.js +1 -1
- package/trades/manage_trade.js +4 -4
- 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,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
|
+
};
|