paid-services 6.1.5 → 6.1.7

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 (37) hide show
  1. package/CHANGELOG.md +1 -1
  2. package/groups/assemble/coordinate_group.js +11 -5
  3. package/groups/assemble_channel_group.js +15 -1
  4. package/groups/estimate_fee_rate.js +38 -0
  5. package/groups/fanout/assemble_fanout_group.js +185 -0
  6. package/groups/fanout/confirm_fanout_join.js +156 -0
  7. package/groups/fanout/coordinate_fanout.js +348 -0
  8. package/groups/fanout/create_group_fanout.js +323 -0
  9. package/groups/fanout/get_fanout_details.js +84 -0
  10. package/groups/fanout/get_fanout_funding.js +183 -0
  11. package/groups/fanout/index.js +4 -0
  12. package/groups/fanout/join_fanout.js +168 -0
  13. package/groups/fanout/join_group_fanout.js +207 -0
  14. package/groups/funding/sign_and_fund_group_channel.js +8 -228
  15. package/groups/index.js +9 -1
  16. package/groups/messages/decode_fanout_details.js +106 -0
  17. package/groups/messages/decode_fanout_proposal.js +84 -0
  18. package/groups/messages/decode_group_details.js +2 -2
  19. package/groups/messages/decode_pending_proposal.js +3 -103
  20. package/groups/messages/decode_signed_funding.js +18 -2
  21. package/groups/messages/decode_utxos_record.js +135 -0
  22. package/groups/messages/encode_fanout_details.js +35 -0
  23. package/groups/messages/encode_fanout_proposal.js +87 -0
  24. package/groups/messages/encode_group_details.js +1 -1
  25. package/groups/messages/index.js +8 -0
  26. package/groups/p2p/get_group_details.js +5 -2
  27. package/groups/p2p/index.js +4 -0
  28. package/groups/p2p/register_fanout_proposal.js +235 -0
  29. package/groups/p2p/register_fanout_signed.js +118 -0
  30. package/groups/p2p/register_group_connected.js +4 -0
  31. package/groups/p2p/register_pending_open.js +11 -0
  32. package/groups/transaction_fee_rate.js +29 -0
  33. package/index.js +4 -0
  34. package/package.json +6 -6
  35. package/service_types.json +7 -1
  36. package/test/integration/test_fanout.js +220 -0
  37. package/groups/assemble/assemble_unsigned_psbt.js +0 -171
package/CHANGELOG.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Versions
2
2
 
3
- ## Version 6.1.5
3
+ ## Version 6.1.7
4
4
 
5
5
  - `manageSwap`: Add support for Loop Out to external address
6
6
 
@@ -1,7 +1,8 @@
1
1
  const EventEmitter = require('events');
2
2
  const {randomBytes} = require('crypto');
3
3
 
4
- const assembleUnsignedPsbt = require('./assemble_unsigned_psbt');
4
+ const {assembleUnsignedPsbt} = require('ln-sync');
5
+
5
6
  const {decodeGroupDetails} = require('./../messages');
6
7
  const {decodePendingProposal} = require('./../messages');
7
8
  const {decodeSignedFunding} = require('./../messages');
@@ -32,6 +33,7 @@ const uniq = arr => Array.from(new Set(arr));
32
33
  {
33
34
  capacity: <Channel Capacity Tokens Number>
34
35
  count: <Group Members Count Number>
36
+ ecp: <ECPair Library Object>
35
37
  identity: <Coordinator Identity Public Key Hex String>
36
38
  lnd: <Authenticated LND API Object>
37
39
  [members]: [<Member Node Id Public Key Hex String>]
@@ -95,11 +97,15 @@ const uniq = arr => Array.from(new Set(arr));
95
97
  // All members have submitted their partial signatures
96
98
  @event 'signed'
97
99
  */
98
- module.exports = ({capacity, count, identity, lnd, members, rate}) => {
100
+ module.exports = ({capacity, count, ecp, identity, lnd, members, rate}) => {
99
101
  if (count < minGroupCount) {
100
102
  throw new Error('ExpectedHigherGroupMembersCountToCoordinateGroup');
101
103
  }
102
104
 
105
+ if (!ecp) {
106
+ throw new Error('ExpectedEcpLibraryToCoordinateChannelGroup');
107
+ }
108
+
103
109
  if (!identity) {
104
110
  throw new Error('ExpectedSelfIdentityPublicKeyToCoordinateGroup');
105
111
  }
@@ -307,7 +313,7 @@ module.exports = ({capacity, count, identity, lnd, members, rate}) => {
307
313
 
308
314
  group.proposed.push({
309
315
  change: proposal.change,
310
- funding: proposal.funding,
316
+ funding: [proposal.funding],
311
317
  id: req.from,
312
318
  utxos: proposal.utxos,
313
319
  });
@@ -371,12 +377,12 @@ module.exports = ({capacity, count, identity, lnd, members, rate}) => {
371
377
  }
372
378
 
373
379
  try {
374
- decodeSignedFunding({records: req.records});
380
+ decodeSignedFunding({ecp, records: req.records});
375
381
  } catch (err) {
376
382
  return res.failure([400, err.message]);
377
383
  }
378
384
 
379
- const signed = decodeSignedFunding({records: req.records});
385
+ const signed = decodeSignedFunding({ecp, records: req.records});
380
386
 
381
387
  // Register the signed funding
382
388
  if (!group.signed.find(n => n.id === req.from)) {
@@ -13,6 +13,7 @@ const {coordinateGroup} = require('./assemble');
13
13
  const {peerWithPartners} = require('./p2p');
14
14
  const {proposeGroupChannel} = require('./funding');
15
15
  const {signAndFundGroupChannel} = require('./funding');
16
+ const transactionFeeRate = require('./transaction_fee_rate');
16
17
 
17
18
  const {fromHex} = Transaction;
18
19
  const interval = 500;
@@ -82,6 +83,7 @@ module.exports = ({capacity, count, ecp, identity, lnd, members, rate}) => {
82
83
  const coordinator = coordinateGroup({
83
84
  capacity,
84
85
  count,
86
+ ecp,
85
87
  identity,
86
88
  lnd,
87
89
  members,
@@ -144,7 +146,12 @@ module.exports = ({capacity, count, ecp, identity, lnd, members, rate}) => {
144
146
  pending.utxos = utxos;
145
147
 
146
148
  // Register as proposed
147
- return coordinator.proposed({change, funding, utxos, id: identity});
149
+ return coordinator.proposed({
150
+ change,
151
+ utxos,
152
+ funding: !!funding ? [funding] : undefined,
153
+ id: identity,
154
+ });
148
155
  } catch (err) {
149
156
  return errored(err);
150
157
  }
@@ -203,6 +210,13 @@ module.exports = ({capacity, count, ecp, identity, lnd, members, rate}) => {
203
210
  // Pull out the raw transaction from the PSBT
204
211
  const {transaction} = extractTransaction({ecp, psbt: finalized.psbt});
205
212
 
213
+ const {inputs} = decodePsbt({ecp, psbt: combined.psbt});
214
+
215
+ // Make sure the final transaction fee rate is not too low
216
+ if (transactionFeeRate({inputs, transaction}).rate < rate) {
217
+ throw [503, 'UnexpectedLowFeeRateForChannelGroupTransaction'];
218
+ }
219
+
206
220
  emitter.emit('broadcasting', ({transaction}));
207
221
 
208
222
  const {id} = await broadcastChainTransaction({lnd, transaction});
@@ -0,0 +1,38 @@
1
+ const {Transaction} = require('bitcoinjs-lib');
2
+
3
+ const {fromHex} = Transaction;
4
+ const isUtxoP2wpkh = utxo => utxo.witness_utxo.script_pub.startsWith('0014');
5
+ const sumTokens = arr => arr.reduce((sum, n) => sum + n.tokens, 0);
6
+ const dummyP2wpkh = [Buffer.alloc(33), Buffer.alloc(72)];
7
+ const dummyP2tr = [Buffer.alloc(64)];
8
+
9
+ /** Assuming large ECDSA signatures, determine a chain fee rate for a PSBT
10
+
11
+ {
12
+ inputs: [{
13
+ witness_utxo: {
14
+ script_pub: <P2TR or P2WPKH Output Script Hex String>
15
+ tokens: <Input Value Tokens Number>
16
+ }
17
+ }]
18
+ outputs: [{
19
+ tokens: <Output Value Tokens Number>
20
+ }]
21
+ unsigned: <Unsigned Transaction Hex String>
22
+ }
23
+
24
+ @returns
25
+ {
26
+ rate: <Estimated Chain Fee Tokens Per VByte Number>
27
+ }
28
+ */
29
+ module.exports = ({inputs, outputs, unsigned}) => {
30
+ const fee = sumTokens(inputs.map(n => n.witness_utxo)) - sumTokens(outputs);
31
+ const tx = fromHex(unsigned);
32
+
33
+ inputs.forEach((input, index) => {
34
+ return tx.setWitness(index, isUtxoP2wpkh(input) ? dummyP2wpkh : dummyP2tr);
35
+ });
36
+
37
+ return {rate: fee / tx.virtualSize()};
38
+ };
@@ -0,0 +1,185 @@
1
+ const EventEmitter = require('events');
2
+
3
+ const {broadcastChainTransaction} = require('ln-service');
4
+ const {combinePsbts} = require('psbt');
5
+ const {decodePsbt} = require('psbt');
6
+ const {extractTransaction} = require('psbt');
7
+ const {finalizePsbt} = require('psbt');
8
+ const {signAndFundPsbt} = require('ln-sync');
9
+
10
+ const coordinateFanout = require('./coordinate_fanout');
11
+ const getFanoutFunding = require('./get_fanout_funding');
12
+ const transactionFeeRate = require('./../transaction_fee_rate');
13
+
14
+ /** Assemble a fanout group
15
+
16
+ This group type must have at least 3 participants
17
+
18
+ {
19
+ capacity: <Output Size Tokens Number>
20
+ count: <Fanout Members Count Number>
21
+ ecp: <ECPair Library Object>
22
+ identity: <Coordinator Identity Public Key Hex String>
23
+ inputs: [<Utxo Outpoint String>]
24
+ lnd: <Authenticated LND API Object>
25
+ [members]: [<Member Identity Public Key Hex String>]
26
+ outputs: <Output Count Number>
27
+ rate: <Chain Fee Tokens Per VByte Number>
28
+ }
29
+
30
+ @returns
31
+ {
32
+ events: <Fanout Status Notifications EventEmitter Object>
33
+ id: <Group Identifier Hex Encoded String>
34
+ }
35
+
36
+ // The fanout transaction was successfully published to relays
37
+ @event 'broadcast'
38
+ {
39
+ id: <Transaction Id Hex String>
40
+ transaction: <Transaction Hex String>
41
+ }
42
+
43
+ // The fanout transaction is being published to relays
44
+ @event 'broadcasting'
45
+ {
46
+ transaction: <Transaction Hex String>
47
+ }
48
+
49
+ // A member is peered with the coordinator
50
+ @event 'connected'
51
+ {}
52
+
53
+ // All members are peered with the coordinator
54
+ @event 'filled'
55
+ {
56
+ ids: [<Group Member Identity Public Key Hex String>]
57
+ }
58
+
59
+ // A member sent a presence update
60
+ @event 'present'
61
+ {
62
+ id: <Present Member Public Id Hex String>
63
+ }
64
+
65
+ // Members have proposed the entire unsigned fanout to the coordinator
66
+ @event 'proposed'
67
+ {}
68
+
69
+ // Members have all signed the fanout transaction
70
+ @event 'signed'
71
+ {}
72
+ */
73
+ module.exports = args => {
74
+ const coordinator = coordinateFanout({
75
+ capacity: args.capacity,
76
+ count: args.count,
77
+ ecp: args.ecp,
78
+ identity: args.identity,
79
+ lnd: args.lnd,
80
+ members: args.members,
81
+ rate: args.rate,
82
+ });
83
+
84
+ const emitter = new EventEmitter();
85
+ const pending = {};
86
+
87
+ const errored = err => {
88
+ coordinator.events.removeAllListeners();
89
+
90
+ return emitter.emit('error', err);
91
+ };
92
+
93
+ // An error was encountered
94
+ coordinator.events.once('error', errored);
95
+
96
+ // Group members have registered themselves
97
+ coordinator.events.once('joined', ({ids}) => emitter.emit('filled', {ids}));
98
+
99
+ // Group members have connected to the coordinator
100
+ coordinator.events.once('connected', async () => {
101
+ emitter.emit('connected', {});
102
+
103
+ try {
104
+ // Get the self funding details for the fanout
105
+ const {change, funding, utxos} = await getFanoutFunding({
106
+ capacity: args.capacity,
107
+ inputs: args.inputs,
108
+ lnd: args.lnd,
109
+ outputs: args.outputs,
110
+ rate: args.rate,
111
+ });
112
+
113
+ pending.utxos = utxos;
114
+
115
+ // Register self as proposed
116
+ return coordinator.proposed({change, funding, utxos, id: args.identity});
117
+ } catch (err) {
118
+ return errored(err);
119
+ }
120
+ });
121
+
122
+ // Group members have proposed fanout to the coordinator
123
+ coordinator.events.once('funded', async () => {
124
+ emitter.emit('proposed', {unsigned: coordinator.unsigned()});
125
+
126
+ try {
127
+ // Sign the unsigned funding transaction
128
+ const signed = await signAndFundPsbt({
129
+ lnd: args.lnd,
130
+ psbt: coordinator.unsigned(),
131
+ utxos: pending.utxos,
132
+ });
133
+
134
+ // Register the signature with the coordinator
135
+ return coordinator.sign({id: args.identity, signed: signed.psbt});
136
+ } catch (err) {
137
+ return errored(err);
138
+ }
139
+ });
140
+
141
+ // Relay presence notifications
142
+ coordinator.events.on('present', ({id}) => emitter.emit('present', {id}));
143
+
144
+ // Group members have submitted their partial signatures
145
+ coordinator.events.once('signed', async () => {
146
+ // Collect all the partially signed PSBTs to be combined into a single PSBT
147
+ const psbts = coordinator.signed().map(n => n.signed);
148
+
149
+ emitter.emit('signed', {psbts});
150
+
151
+ try {
152
+ // Merge all the partial signed PSBTs into a single PSBT with all sigs
153
+ const combined = combinePsbts({psbts, ecp: args.ecp});
154
+
155
+ // Finalize the PSBT to convert partial signatures to final signatures
156
+ const finalized = finalizePsbt({ecp: args.ecp, psbt: combined.psbt});
157
+
158
+ // Pull out the raw transaction from the PSBT
159
+ const {transaction} = extractTransaction({
160
+ ecp: args.ecp,
161
+ psbt: finalized.psbt,
162
+ });
163
+
164
+ const {inputs} = decodePsbt({ecp: args.ecp, psbt: combined.psbt});
165
+
166
+ // Make sure the final transaction fee rate is not too low
167
+ if (transactionFeeRate({inputs, transaction}).rate < args.rate) {
168
+ throw [503, 'UnexpectedLowFeeRateForFanoutTransaction'];
169
+ }
170
+
171
+ emitter.emit('broadcasting', ({transaction}));
172
+
173
+ const {id} = await broadcastChainTransaction({
174
+ transaction,
175
+ lnd: args.lnd,
176
+ });
177
+
178
+ return emitter.emit('broadcast', {id, transaction});
179
+ } catch (err) {
180
+ return errored(err);
181
+ }
182
+ });
183
+
184
+ return {events: emitter, id: coordinator.id};
185
+ };
@@ -0,0 +1,156 @@
1
+ const asyncAuto = require('async/auto');
2
+ const asyncRetry = require('async/retry');
3
+ const asyncTimeout = require('async/timeout');
4
+ const {connectPeer} = require('ln-sync');
5
+ const {getChainBalance} = require('ln-service');
6
+ const {getNodeAlias} = require('ln-sync');
7
+ const {returnResult} = require('asyncjs-util');
8
+
9
+ const getFanoutDetails = require('./get_fanout_details');
10
+
11
+ const coordinatorFromJoinCode = n => n.slice(0, 66);
12
+ const defaultTimeoutMs = 1000 * 60;
13
+ const groupIdFromJoinCode = n => n.slice(66);
14
+ const interval = 500;
15
+ const isCode = n => !!n && n.length === 98;
16
+ const isNumber = n => !isNaN(n);
17
+ const isPublicKey = n => !!n && /^0[2-3][0-9A-F]{64}$/i.test(n);
18
+ const join = arr => arr.join(' ');
19
+ const niceName = n => `${n.alias} ${n.id}`.trim();
20
+ const times = 2 * 60;
21
+ const tokensAsBigUnit = tokens => (tokens / 1e8).toFixed(8);
22
+
23
+ /** Ask to confirm joining a group
24
+
25
+ {
26
+ code: <Group Invite Code String>
27
+ count: <Output Count Number>
28
+ lnd: <Authenticated LND API Object>
29
+ logger: <Winston Logger Object>
30
+ }
31
+
32
+ @returns via cbk or Promise
33
+ {
34
+ capacity: <Fanout Output Capacity Tokens Number>
35
+ coordinator: <Group Coordinator Identity Public Key Hex String>
36
+ count: <Group Members Count>
37
+ id: <Group Id Hex String>
38
+ rate: <Chain Fee Tokens Per VByte Number>
39
+ }
40
+ */
41
+ module.exports = ({code, count, lnd, logger}, cbk) => {
42
+ return new Promise((resolve, reject) => {
43
+ return asyncAuto({
44
+ // Check arguments
45
+ validate: cbk => {
46
+ if (!isCode(code)) {
47
+ return cbk([400, 'ExpectedFanoutGroupInviteCodeToGetJoinDetails']);
48
+ }
49
+
50
+ if (!count || !isNumber(count)) {
51
+ return cbk([400, 'ExpectedOutputCountToJoinGroupFanout']);
52
+ }
53
+
54
+ if (!lnd) {
55
+ return cbk([400, 'ExpectedAuthenticatedLndToGetJoinGroupDetails']);
56
+ }
57
+
58
+ if (!logger) {
59
+ return cbk([400, 'ExpectedWinstonLoggerObjectToGetJoinDetails']);
60
+ }
61
+
62
+ return cbk();
63
+ },
64
+
65
+ // Get the wallet balance to make sure there are enough funds to join
66
+ getBalance: ['validate', ({}, cbk) => getChainBalance({lnd}, cbk)],
67
+
68
+ // Parse the group join code
69
+ group: ['validate', ({}, cbk) => {
70
+ const coordinator = coordinatorFromJoinCode(code);
71
+
72
+ if (!isPublicKey(coordinator)) {
73
+ return cbk([400, 'ExpectedValidGroupJoinCodeToRequestGroupDetails']);
74
+ }
75
+
76
+ const id = groupIdFromJoinCode(code);
77
+
78
+ return cbk(null, {coordinator, id});
79
+ }],
80
+
81
+ // Connect to the coordinator
82
+ connect: ['group', ({group}, cbk) => {
83
+ return asyncRetry({interval, times}, cbk => {
84
+ logger.info({connecting_to: group.coordinator});
85
+
86
+ return asyncTimeout(connectPeer, defaultTimeoutMs)({
87
+ lnd,
88
+ id: group.coordinator,
89
+ },
90
+ cbk);
91
+ },
92
+ cbk);
93
+ }],
94
+
95
+ // Get the coordinator node alias to log it out
96
+ getAlias: ['group', ({group}, cbk) => {
97
+ return getNodeAlias({lnd, id: group.coordinator}, cbk);
98
+ }],
99
+
100
+ // Get the group details from the coordinator once connected
101
+ getDetails: ['connect', 'group', ({group}, cbk) => {
102
+ return asyncRetry({interval, times}, cbk => {
103
+ logger.info({requesting_group_details_from: group.coordinator});
104
+
105
+ return getFanoutDetails({
106
+ lnd,
107
+ coordinator: group.coordinator,
108
+ id: group.id,
109
+ },
110
+ cbk);
111
+ },
112
+ cbk);
113
+ }],
114
+
115
+ // Log the details of the group being joined
116
+ log: [
117
+ 'getAlias',
118
+ 'getBalance',
119
+ 'getDetails',
120
+ ({getAlias, getBalance, getDetails}, cbk) =>
121
+ {
122
+ const coordinatedBy = `coordinated by ${niceName(getAlias)}`;
123
+ const members = `${getDetails.count} member fanout group`;
124
+ const size = `with ${tokensAsBigUnit(getDetails.capacity)} outputs`;
125
+ const rate = `and paying ${getDetails.rate}/vbyte chain fee`;
126
+
127
+ logger.info({joining: join([members, coordinatedBy, size, rate])});
128
+
129
+ const requiredFunding = getDetails.funding * count;
130
+
131
+ // Check to make sure that there are on chain funds for this group
132
+ if (getBalance.chain_balance < requiredFunding) {
133
+ return cbk([
134
+ 400,
135
+ 'InsufficientChainFundsAvailableToJoinGroup',
136
+ {chain_balance: getBalance.chain_balance},
137
+ ]);
138
+ }
139
+
140
+ return cbk();
141
+ }],
142
+
143
+ // Go ahead with the group join
144
+ join: ['getDetails', 'group', ({getDetails, group}, cbk) => {
145
+ return cbk(null, {
146
+ capacity: getDetails.capacity,
147
+ coordinator: group.coordinator,
148
+ count: getDetails.count,
149
+ id: group.id,
150
+ rate: getDetails.rate,
151
+ });
152
+ }],
153
+ },
154
+ returnResult({reject, resolve, of: 'join'}, cbk));
155
+ });
156
+ };